146 lines
6.2 KiB
Python
146 lines
6.2 KiB
Python
import io
|
|
import json
|
|
import unittest
|
|
|
|
from h8536.protocol_capture import analyze_capture_text, format_text_report, main, parse_capture_text
|
|
|
|
|
|
class ProtocolCaptureTest(unittest.TestCase):
|
|
def test_parses_timestamped_capture_chunks(self):
|
|
chunks = parse_capture_text("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n")
|
|
|
|
self.assertEqual(len(chunks), 1)
|
|
self.assertEqual(chunks[0].timestamp_ms, 57975502)
|
|
self.assertEqual(chunks[0].analyzer_direction, "rx")
|
|
self.assertEqual(chunks[0].device_direction, "tx")
|
|
self.assertEqual(chunks[0].bytes, (0x00, 0x00, 0x15, 0x80, 0x00, 0xCF))
|
|
|
|
def test_recombines_user_split_rx_chunks_into_valid_call_frame(self):
|
|
analysis = analyze_capture_text(
|
|
"16:06:15.502 RX 003 bytes 00 00 15\n"
|
|
"16:06:15.506 RX 003 bytes 80 00 CF\n"
|
|
)
|
|
|
|
self.assertEqual(analysis["frame_count"], 1)
|
|
frame = analysis["frames"][0]
|
|
self.assertEqual(frame["source_chunk_indexes"], [0, 1])
|
|
self.assertEqual(frame["analyzer_direction"], "rx")
|
|
self.assertEqual(frame["device_direction"], "tx")
|
|
self.assertEqual(frame["report_candidate"]["index"], 0x15)
|
|
self.assertEqual(frame["report_candidate"]["value"], 0x8000)
|
|
self.assertEqual(
|
|
frame["report_candidate"]["observed_candidate"]["name_candidate"],
|
|
"call_button_candidate",
|
|
)
|
|
self.assertEqual(frame["report_candidate"]["observed_candidate"]["state_candidate"], "active")
|
|
|
|
def test_recombines_split_chunks_with_multiple_frames_and_labels_known_reports(self):
|
|
analysis = analyze_capture_text(
|
|
"16:06:15.500 RX 004 bytes 00 00 00 00\n"
|
|
"16:06:15.502 RX 005 bytes 80 DA 00 00 07\n"
|
|
"16:06:15.504 RX 003 bytes 80 00 DD\n"
|
|
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
)
|
|
|
|
self.assertEqual(analysis["frame_count"], 3)
|
|
names = [
|
|
frame["report_candidate"]["observed_candidate"]["name_candidate"]
|
|
for frame in analysis["frames"]
|
|
]
|
|
self.assertEqual(
|
|
names,
|
|
[
|
|
"heartbeat_alive_candidate",
|
|
"cam_power_button_candidate",
|
|
"heartbeat_alive_candidate",
|
|
],
|
|
)
|
|
self.assertEqual(analysis["repeated_group_count"], 1)
|
|
group = analysis["repeated_groups"][0]
|
|
self.assertEqual(group["count"], 2)
|
|
self.assertEqual(group["cadence_ms"]["samples"], [100])
|
|
|
|
def test_text_report_mentions_split_label_and_cadence(self):
|
|
report = format_text_report(
|
|
analyze_capture_text(
|
|
"16:06:15.502 RX 003 bytes 00 00 15\n"
|
|
"16:06:15.506 RX 003 bytes 80 00 CF\n"
|
|
"16:06:15.602 RX 006 bytes 00 00 15 80 00 CF\n"
|
|
)
|
|
)
|
|
|
|
self.assertIn("call_button_candidate state=active", report)
|
|
self.assertIn("checksum=ok split", report)
|
|
self.assertIn("cadence=avg=100.0ms", report)
|
|
|
|
def test_gate_session_hints_summarize_reports_transitions_and_heartbeat_interruptions(self):
|
|
analysis = analyze_capture_text(
|
|
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
|
|
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
|
|
"16:06:15.700 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
)
|
|
|
|
hints = analysis["gate_session_hints"]
|
|
self.assertEqual(
|
|
hints["observed_autonomous_report_names"],
|
|
["call_button_candidate", "heartbeat_alive_candidate"],
|
|
)
|
|
reports = {item["name_candidate"]: item for item in hints["observed_reports"]}
|
|
self.assertEqual(reports["heartbeat_alive_candidate"]["count"], 3)
|
|
self.assertEqual(reports["heartbeat_alive_candidate"]["first_timestamp"], "16:06:15.500")
|
|
self.assertEqual(reports["heartbeat_alive_candidate"]["last_timestamp"], "16:06:15.700")
|
|
self.assertEqual(reports["call_button_candidate"]["states"], ["active", "inactive"])
|
|
|
|
self.assertEqual(hints["heartbeat_cadence_ms"]["samples"], [100, 100])
|
|
self.assertEqual(hints["heartbeat_cadence_ms"]["average"], 100.0)
|
|
self.assertEqual(len(hints["heartbeat_interruptions"]), 2)
|
|
self.assertEqual(
|
|
hints["heartbeat_interruptions"][0]["interrupted_by"][0]["name_candidate"],
|
|
"call_button_candidate",
|
|
)
|
|
|
|
self.assertEqual(len(hints["active_inactive_transitions"]), 1)
|
|
transition = hints["active_inactive_transitions"][0]
|
|
self.assertEqual(transition["index_hex"], "0x0015")
|
|
self.assertEqual(transition["from_state"], "active")
|
|
self.assertEqual(transition["to_state"], "inactive")
|
|
self.assertEqual(hints["evidence_scope"], "capture_side_observation_only")
|
|
self.assertIn("host/session gating", hints["caveat"])
|
|
|
|
def test_text_report_mentions_gate_session_hints_and_caveat(self):
|
|
report = format_text_report(
|
|
analyze_capture_text(
|
|
"16:06:15.500 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
"16:06:15.550 RX 006 bytes 00 00 15 80 00 CF\n"
|
|
"16:06:15.600 RX 006 bytes 00 00 00 00 80 DA\n"
|
|
"16:06:15.650 RX 006 bytes 00 00 15 00 00 4F\n"
|
|
)
|
|
)
|
|
|
|
self.assertIn(
|
|
"observed autonomous report candidates: call_button_candidate, heartbeat_alive_candidate",
|
|
report,
|
|
)
|
|
self.assertIn("heartbeat cadence count=2 cadence=avg=100.0ms", report)
|
|
self.assertIn("transition index=0x0015 active->inactive", report)
|
|
self.assertIn("heartbeat gap 16:06:15.500..16:06:15.600", report)
|
|
self.assertIn("caveat: Missing autonomous reports", report)
|
|
|
|
def test_cli_json_output(self):
|
|
output = io.StringIO()
|
|
rc = main(
|
|
["--json", "-"],
|
|
stdin=io.StringIO("16:06:15.502 RX 006 bytes 00 00 15 80 00 CF\n"),
|
|
stdout=output,
|
|
)
|
|
|
|
self.assertEqual(rc, 0)
|
|
payload = json.loads(output.getvalue())
|
|
self.assertEqual(payload["frames"][0]["report_candidate"]["index"], 0x15)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|