updates
This commit is contained in:
@@ -86,6 +86,14 @@ class BenchConnectLcdTest(unittest.TestCase):
|
||||
def test_label_frame_marks_copy_in_progress_selector_006d_candidate(self):
|
||||
self.assertEqual(label_frame(bytes.fromhex("00006D000037")), "copy_in_progress_selector_006d_candidate")
|
||||
|
||||
def test_label_frame_marks_iris_mblack_link_reports(self):
|
||||
self.assertEqual(label_frame(bytes.fromhex("000013400009")), "known_iris_mblack_link_active_report_candidate")
|
||||
self.assertEqual(label_frame(bytes.fromhex("02001300004B")), "queued_iris_mblack_link_clear_report_candidate")
|
||||
self.assertEqual(
|
||||
label_frame(bytes.fromhex("010013C00088")),
|
||||
"queued_selector_0013_bit15_plus_iris_mblack_link_report_candidate",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import unittest
|
||||
from collections import Counter
|
||||
|
||||
from ccu_emulator.controller import CcuConfig, CcuEmulator
|
||||
from ccu_emulator.frames import ACTIVE_SEED_COMMAND0, NEUTRAL_ACK_FRAME, build_frame, frame_checksum_ok
|
||||
from ccu_emulator.iris_mblack_link import IrisMblackLinkModule
|
||||
from ccu_emulator.policy import AckPolicy
|
||||
from ccu_emulator.refresh import PeriodicRefresh
|
||||
from ccu_emulator.serial_link import RxFrame
|
||||
|
||||
|
||||
class CcuEmulatorFrameTests(unittest.TestCase):
|
||||
@@ -29,5 +33,95 @@ class PeriodicRefreshTests(unittest.TestCase):
|
||||
self.assertEqual(refresh.due_frames(now=10.6), [])
|
||||
|
||||
|
||||
class IrisMblackLinkModuleTests(unittest.TestCase):
|
||||
def test_active_report_gets_selector_ack_and_mirror(self):
|
||||
module = IrisMblackLinkModule(mirror_delay=0.012)
|
||||
|
||||
decision = module.on_rx(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate")
|
||||
|
||||
self.assertIsNotNone(decision)
|
||||
assert decision is not None
|
||||
self.assertTrue(decision.suppress_default_ack)
|
||||
self.assertEqual(
|
||||
[tx.frame for tx in decision.tx],
|
||||
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
|
||||
)
|
||||
self.assertEqual(decision.tx[1].delay, 0.012)
|
||||
self.assertIn("active", decision.reason)
|
||||
|
||||
def test_clear_report_gets_selector_ack_and_mirror(self):
|
||||
module = IrisMblackLinkModule()
|
||||
|
||||
decision = module.on_rx(bytes.fromhex("02001300004B"), "queued_iris_mblack_link_clear_report_candidate")
|
||||
|
||||
self.assertIsNotNone(decision)
|
||||
assert decision is not None
|
||||
self.assertEqual(
|
||||
[tx.frame for tx in decision.tx],
|
||||
[bytes.fromhex("05001300004C"), bytes.fromhex("000013000049")],
|
||||
)
|
||||
self.assertIn("clear", decision.reason)
|
||||
|
||||
def test_non_0013_report_is_ignored(self):
|
||||
module = IrisMblackLinkModule()
|
||||
|
||||
self.assertIsNone(module.on_rx(bytes.fromhex("0000158000CF"), "known_call_button_active_report"))
|
||||
|
||||
|
||||
class CcuEmulatorModuleIntegrationTests(unittest.TestCase):
|
||||
def test_module_response_suppresses_generic_neutral_ack(self):
|
||||
link = FakeLink(RxFrame(bytes.fromhex("000013400009"), "known_iris_mblack_link_active_report_candidate"))
|
||||
logger = FakeLogger()
|
||||
emulator = CcuEmulator(
|
||||
link,
|
||||
logger,
|
||||
config=CcuConfig(seed_frames=(), ready_heartbeats=0),
|
||||
modules=(IrisMblackLinkModule(mirror_delay=0.0),),
|
||||
)
|
||||
|
||||
emulator._service_rx()
|
||||
|
||||
self.assertEqual(
|
||||
[frame for frame, _label in link.sent],
|
||||
[bytes.fromhex("05001300004C"), bytes.fromhex("000013400009")],
|
||||
)
|
||||
self.assertEqual(emulator.stats.ack_frames, 0)
|
||||
self.assertEqual(emulator.stats.module_frames, 2)
|
||||
self.assertEqual(emulator.stats.tx_frames, 2)
|
||||
|
||||
|
||||
class FakeDetector:
|
||||
def __init__(self) -> None:
|
||||
self.labels = Counter()
|
||||
self.resync_events = 0
|
||||
self.dropped_bytes = 0
|
||||
|
||||
|
||||
class FakeLink:
|
||||
def __init__(self, *items: RxFrame) -> None:
|
||||
self.items = list(items)
|
||||
self.sent: list[tuple[bytes, str]] = []
|
||||
self.detector = FakeDetector()
|
||||
|
||||
def read_available(self) -> list[RxFrame]:
|
||||
if not self.items:
|
||||
return []
|
||||
return [self.items.pop(0)]
|
||||
|
||||
def send(self, frame: bytes, label: str) -> None:
|
||||
self.sent.append((frame, label))
|
||||
|
||||
|
||||
class FakeLogger:
|
||||
def __init__(self) -> None:
|
||||
self.lines: list[str] = []
|
||||
|
||||
def event(self, text: str) -> None:
|
||||
self.lines.append(text)
|
||||
|
||||
def emit(self, line: str = "") -> None:
|
||||
self.lines.append(line)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
58
tests/test_panel_selectors.py
Normal file
58
tests/test_panel_selectors.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import unittest
|
||||
|
||||
from h8536.panel_selectors import (
|
||||
CURRENT_TABLE_BASE,
|
||||
describe_selector_value,
|
||||
known_panel_selector,
|
||||
panel_selector_semantics_payload,
|
||||
selector_word_address,
|
||||
)
|
||||
|
||||
|
||||
class PanelSelectorSemanticsTest(unittest.TestCase):
|
||||
def test_selector_0013_maps_to_current_table_word_and_lamp_bits(self):
|
||||
item = known_panel_selector(0x0013)
|
||||
|
||||
self.assertIsNotNone(item)
|
||||
assert item is not None
|
||||
self.assertEqual(item["current_word_address_hex"], "H'E826")
|
||||
self.assertEqual(selector_word_address(CURRENT_TABLE_BASE, 0x0013), 0xE826)
|
||||
|
||||
text = " ".join(describe_selector_value(0x0013, 0x4000))
|
||||
self.assertIn("IRIS/M.BLACK LINK", text)
|
||||
self.assertIn("F791.5", text)
|
||||
self.assertIn("F716.7", text)
|
||||
|
||||
def test_selector_payload_includes_local_panel_toggle_source(self):
|
||||
by_selector = {
|
||||
int(item["selector"]): item
|
||||
for item in panel_selector_semantics_payload()
|
||||
}
|
||||
|
||||
selector_0013 = by_selector[0x0013]
|
||||
trigger_text = " ".join(
|
||||
str(trigger.get("summary", ""))
|
||||
for trigger in selector_0013.get("local_triggers", [])
|
||||
)
|
||||
self.assertIn("F6DB.7", trigger_text)
|
||||
self.assertIn("H'E826", trigger_text)
|
||||
trigger_names = " ".join(
|
||||
str(trigger.get("name_candidate", ""))
|
||||
for trigger in selector_0013.get("local_triggers", [])
|
||||
)
|
||||
self.assertIn("provisional_iris_mblack_link_button_toggle_report", trigger_names)
|
||||
|
||||
def test_selector_payload_includes_closed_loop_state_machine(self):
|
||||
item = known_panel_selector(0x0013)
|
||||
|
||||
self.assertIsNotNone(item)
|
||||
assert item is not None
|
||||
state_machine = item["state_machine"]
|
||||
self.assertEqual(state_machine["name_candidate"], "iris_mblack_link_closed_loop_state_candidate")
|
||||
self.assertEqual(state_machine["ack_frame"], "05 00 13 00 00 4C")
|
||||
self.assertEqual(state_machine["active_mirror_frame"], "00 00 13 40 00 09")
|
||||
self.assertEqual(state_machine["clear_mirror_frame"], "00 00 13 00 00 49")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -494,6 +494,77 @@ class SerialPseudocodeTest(unittest.TestCase):
|
||||
self.assertIn("MEM8[0xF9C6u] = (u8)(MEM8[0xF9C6u] - 1u);", text)
|
||||
self.assertIn("candidate effect: table_write_candidate; target primary_value_table_candidate", text)
|
||||
|
||||
def test_panel_selector_semantics_are_emitted_as_comments_and_helper(self):
|
||||
analysis = {
|
||||
"protocol_semantics": [
|
||||
{
|
||||
"confidence": "medium",
|
||||
"confidence_score": 0.7,
|
||||
"commands": [],
|
||||
"panel_selector_semantics": [
|
||||
{
|
||||
"selector": 0x0013,
|
||||
"selector_hex": "0x0013",
|
||||
"name": "slave_and_iris_mblack_link_lamps",
|
||||
"summary": "Selector 0x0013 reads H'E826 and controls two lamp bits.",
|
||||
"current_word_address_hex": "H'E826",
|
||||
"dispatch_handler": "H'2E06",
|
||||
"state_machine": {
|
||||
"name_candidate": "iris_mblack_link_closed_loop_state_candidate",
|
||||
"summary": "RCP reports local intent, CCU ACKs selector 0x0013, then mirrors accepted state back.",
|
||||
"active_report_frame": "00 00 13 40 00 09",
|
||||
"clear_report_frame": "00 00 13 00 00 49",
|
||||
"ack_frame": "05 00 13 00 00 4C",
|
||||
"active_mirror_frame": "00 00 13 40 00 09",
|
||||
"clear_mirror_frame": "00 00 13 00 00 49",
|
||||
},
|
||||
"effects": [
|
||||
{
|
||||
"bit": 14,
|
||||
"mask": 0x4000,
|
||||
"mask_hex": "0x4000",
|
||||
"name": "IRIS/M.BLACK LINK lamp",
|
||||
"when_set": "sets F791.5 and F716.7",
|
||||
"when_clear": "clears F791.5 and F716.7",
|
||||
"ram_bits": ["F791.5", "F716.7"],
|
||||
},
|
||||
],
|
||||
"local_triggers": [
|
||||
{
|
||||
"source": "F006.7 / F6DB.7",
|
||||
"handler": "H'200E",
|
||||
"name_candidate": "provisional_iris_mblack_link_button_toggle_report",
|
||||
"summary": "H'200E toggles H'E826 bit14 and queues selector 0x0013.",
|
||||
"gate": "F731 <= 3",
|
||||
"current_state_bit": "F791.5",
|
||||
"active_value": 0x4000,
|
||||
"clear_value": 0x0000,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("h8536.serial_pseudocode.analyze_serial_semantics", return_value=analysis):
|
||||
text = generate_serial_pseudocode(candidate_payload())
|
||||
|
||||
self.assertIn("panel selector semantics:", text)
|
||||
self.assertIn("0x0013 slave_and_iris_mblack_link_lamps", text)
|
||||
self.assertIn("0x4000 -> IRIS/M.BLACK LINK lamp", text)
|
||||
self.assertIn("F791.5", text)
|
||||
self.assertIn("F716.7", text)
|
||||
self.assertIn("F006.7 / F6DB.7", text)
|
||||
self.assertIn("iris_mblack_link_closed_loop_state_candidate", text)
|
||||
self.assertIn("ACK 05 00 13 00 00 4C", text)
|
||||
self.assertIn("static void sci1_candidate_panel_selector_annotation", text)
|
||||
self.assertIn("case 0x0013u:", text)
|
||||
self.assertIn("void provisional_iris_mblack_link_button_toggle_report(void)", text)
|
||||
self.assertIn("Source F006.7 / F6DB.7; gate F731 <= 3; current state F791.5.", text)
|
||||
self.assertIn("Requests selector 0x0013=0x4000", text)
|
||||
self.assertIn("CCU should ACK 05 00 13 00 00 4C, then mirror 00 00 13 40 00 09.", text)
|
||||
|
||||
def test_timer_source_models_emit_separate_tick_isrs(self):
|
||||
analysis = {
|
||||
"protocol_semantics": [
|
||||
|
||||
@@ -84,6 +84,67 @@ class SerialScenarioTest(unittest.TestCase):
|
||||
self.assertIn("before_send=1", output)
|
||||
self.assertIn("delays=0,0.25,1", output)
|
||||
|
||||
def test_dry_run_summarizes_listen_ack_until_quiet(self):
|
||||
scenario = {
|
||||
"name": "unit-quiet-ack",
|
||||
"steps": [
|
||||
{
|
||||
"action": "listen_ack_until_quiet",
|
||||
"seconds": 12.0,
|
||||
"quiet_seconds": 0.9,
|
||||
"target_mode": "queued_reports",
|
||||
"ack_mode": "cmd5_selector",
|
||||
"max_acks": 16,
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir) / "scenario.json"
|
||||
path.write_text(json.dumps(scenario), encoding="utf-8")
|
||||
stdout = io.StringIO()
|
||||
|
||||
exit_code = main([str(path), "--dry-run"], stdout=stdout)
|
||||
|
||||
output = stdout.getvalue()
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("step[1]=listen_ack_until_quiet", output)
|
||||
self.assertIn("seconds=12.000", output)
|
||||
self.assertIn("quiet=0.900s", output)
|
||||
self.assertIn("target_mode=queued_reports", output)
|
||||
self.assertIn("ack_mode=cmd5_selector", output)
|
||||
|
||||
def test_dry_run_summarizes_respond_on_rules(self):
|
||||
scenario = {
|
||||
"name": "unit-respond-on",
|
||||
"steps": [
|
||||
{
|
||||
"action": "listen_ack",
|
||||
"seconds": 2.0,
|
||||
"target_mode": "queued_reports",
|
||||
"ack_mode": "cmd5_selector",
|
||||
"respond_on": [
|
||||
{
|
||||
"frames": ["00 00 13 40 00 09"],
|
||||
"send": "00 00 13 40 00 09",
|
||||
"label": "mirror_selector_0013_active_from_button",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = Path(tmpdir) / "scenario.json"
|
||||
path.write_text(json.dumps(scenario), encoding="utf-8")
|
||||
stdout = io.StringIO()
|
||||
|
||||
exit_code = main([str(path), "--dry-run"], stdout=stdout)
|
||||
|
||||
output = stdout.getvalue()
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertIn("respond_on=1", output)
|
||||
self.assertIn("send=00 00 13 40 00 09", output)
|
||||
self.assertIn("label=mirror_selector_0013_active_from_button", output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
20
tests/test_serial_scenario_unexpected.py
Normal file
20
tests/test_serial_scenario_unexpected.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import unittest
|
||||
|
||||
from h8536.serial_scenario_unexpected import parse_detected_frames
|
||||
|
||||
|
||||
class SerialScenarioUnexpectedTest(unittest.TestCase):
|
||||
def test_parse_detected_frames_relabels_iris_mblack_link_report(self):
|
||||
frames = parse_detected_frames(
|
||||
[
|
||||
"12:00:00.000 DETECT checksum_ok_unlabeled 00 00 13 40 00 09",
|
||||
"12:00:00.100 DETECT checksum_ok_unlabeled 02 00 13 00 00 4B",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(frames[0].label, "known_iris_mblack_link_active_report_candidate")
|
||||
self.assertEqual(frames[1].label, "queued_iris_mblack_link_clear_report_candidate")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -405,6 +405,19 @@ class SerialSemanticsTest(unittest.TestCase):
|
||||
self.assertIn("faa2 == 0", gate_text)
|
||||
self.assertIn("not rom constants", gate_text)
|
||||
|
||||
def test_panel_selector_semantics_include_iris_mblack_link_trace(self):
|
||||
semantics = only_semantics(self, planned_semantics_payload())
|
||||
|
||||
panel = semantics["panel_selector_semantics"]
|
||||
panel_text = semantic_text(panel)
|
||||
|
||||
self.assertIn("0x0013", panel_text)
|
||||
self.assertIn("iris/m.black link", panel_text)
|
||||
self.assertIn("h'e826", panel_text)
|
||||
self.assertIn("f791.5", panel_text)
|
||||
self.assertIn("f716.7", panel_text)
|
||||
self.assertIn("f6db.7", panel_text)
|
||||
|
||||
def test_actual_dispatch_split_marks_initial_and_continuation_commands(self):
|
||||
semantics = only_semantics(
|
||||
self,
|
||||
|
||||
@@ -130,9 +130,9 @@ class TableXrefsTest(unittest.TestCase):
|
||||
|
||||
self.assertIn("Table/Index Cross-Reference Report for sample.json", text)
|
||||
self.assertIn("primary_value_table_candidate H'E000", text)
|
||||
self.assertIn("offset H'0006 -> H'E006", text)
|
||||
self.assertIn("offset H'0124 -> H'E124", text)
|
||||
self.assertIn("offset H'0002 at H'F922", text)
|
||||
self.assertIn("offset H'0006 selector 0x003 -> H'E006", text)
|
||||
self.assertIn("offset H'0124 selector 0x092 -> H'E124", text)
|
||||
self.assertIn("offset H'0002 selector 0x001 at H'F922", text)
|
||||
self.assertIn("index dynamic via R4", text)
|
||||
self.assertIn("term 'CONNECT': no LCD/text candidate hits", text)
|
||||
self.assertIn("term 'COMM LINK': 1 candidate", text)
|
||||
|
||||
Reference in New Issue
Block a user