summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/tests
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/tests')
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_libos_port.py186
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_msgq_attach.py114
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_state_decode.py331
3 files changed, 616 insertions, 15 deletions
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_libos_port.py b/debug/risc-v-gsp-rm-vm/tests/test_libos_port.py
index 0f01c01..befa14f 100644
--- a/debug/risc-v-gsp-rm-vm/tests/test_libos_port.py
+++ b/debug/risc-v-gsp-rm-vm/tests/test_libos_port.py
@@ -29,11 +29,15 @@ if TOOL_ROOT not in sys.path:
from sim.libos import INTERRUPT_ARM_EXTERNAL, TASK_PORT_SEND_RECV_WAIT # noqa: E402
from sim.libos_port import ( # noqa: E402
EXTINTR_WAKE_CONTRACT,
+ EXTERNAL_INTERRUPT_XCAUSE,
HANDSHAKE_CONTRACT,
+ NETWORK_REPORT_FORMAT,
REPORT_FORMAT,
SHUTTLE_ANY,
+ SPURIOUS_XEPC,
TIMEOUT_INFINITE,
LibosPortModel,
+ LibosPortNetwork,
role_for,
)
@@ -50,6 +54,26 @@ class FakeMachine:
self.x[index] = value & 0xFFFFFFFFFFFFFFFF
+class FakeMemoryMachine(FakeMachine):
+ """Register file plus flat memory for the routing model tests."""
+
+ def __init__(self, size: int = 0x10000):
+ super().__init__()
+ self.ram = bytearray(size)
+ self.ram_base = 0
+ self.ram_size = size
+
+ def read_mem(self, addr: int, size: int) -> bytes:
+ if addr < 0 or addr + size > self.ram_size:
+ raise ValueError("out of range")
+ return bytes(self.ram[addr:addr + size])
+
+ def write_mem(self, addr: int, data: bytes) -> None:
+ if addr < 0 or addr + len(data) > self.ram_size:
+ raise ValueError("out of range")
+ self.ram[addr:addr + len(data)] = data
+
+
class FakeBase:
"""Minimal base syscall model: handles arm, records and stops the rest."""
@@ -287,5 +311,167 @@ class StateTests(unittest.TestCase):
self.assertNotIn(str(key), ("t", "ts", "timestamp", "time"))
+class SyncPortTests(unittest.TestCase):
+ """Synchronous receive semantics (waitId != 0 must block, not fake OK)."""
+
+ def test_sync_recv_without_completion_blocks(self):
+ model = LibosPortModel(base=FakeBase())
+ machine = FakeMachine()
+ _port_call(machine, recv=(9, 8, 0x100, 0x10), wait=9)
+ handled, result = model.handle(machine, pc=0x4019B00)
+ self.assertFalse(handled)
+ self.assertIsNone(result)
+ self.assertEqual(model.stopped["reason"], "sync-recv-blocks")
+ self.assertEqual(model.waits[-1]["reason"], "sync-recv-blocks")
+ self.assertEqual(model.waits[-1]["shuttle"], 9)
+
+ def test_sync_recv_returns_a_queued_completion(self):
+ model = LibosPortModel(base=FakeBase())
+ machine = FakeMachine()
+ model.host_kick()
+ _port_call(machine, recv=(10, 6, 0, 0))
+ model.handle(machine, pc=0x4018038)
+ machine.set(5, INTERRUPT_ARM_EXTERNAL)
+ machine.set(10, 0xFF)
+ model.handle(machine, pc=0x4018054)
+ _port_call(machine, recv=(9, 8, 0x200, 0x10), wait=9)
+ handled, result = model.handle(machine, pc=0x4019B00)
+ self.assertTrue(handled)
+ self.assertEqual(result, 0)
+ self.assertEqual(machine.x[16], 10)
+ self.assertEqual(model.waits[-1]["completed"], 10)
+
+
+class NetworkModelTests(unittest.TestCase):
+ """The two-task routing model (opt-in, additive)."""
+
+ def _network(self):
+ return LibosPortNetwork(base=FakeBase(), tasks=("init", "rm"),
+ current="init")
+
+ def test_state_format_is_the_network_schema(self):
+ model = self._network()
+ self.assertEqual(model.state()["format"], NETWORK_REPORT_FORMAT)
+ self.assertEqual(model.state()["tasks"], ["init", "rm"])
+
+ def test_cross_task_send_copies_the_firmware_payload(self):
+ model = self._network()
+ machine = FakeMemoryMachine()
+ model.switch("init", pc=0)
+ _port_call(machine, recv=(9, 8, 0x1000, 0x808))
+ model.handle(machine, pc=0x4019B00)
+ machine.write_mem(0x2000, b"\x11\x22\x33\x44")
+ model.switch("rm", pc=0)
+ _port_call(machine, send=(1, 8, 0x2000, 4), recv=(2, 11, 0x3000, 0x100),
+ wait=2)
+ handled, _ = model.handle(machine, pc=0x5AD507C)
+ self.assertFalse(handled) # the sync receive blocks
+ self.assertEqual(model.stopped["reason"], "sync-recv-blocks")
+ completions = model.task_completions["init"]
+ self.assertEqual(len(completions), 1)
+ self.assertEqual(completions[0]["payload_size"], 4)
+ self.assertEqual(completions[0]["origin_task"], 1)
+ self.assertEqual(machine.read_mem(0x1000, 4), b"\x11\x22\x33\x44")
+ self.assertEqual(model.cross_task_payloads,
+ [{"port": 8, "size": 4, "from": "rm", "to": "init",
+ "kind": "send"}])
+
+ def test_reply_completes_the_sender_receive_and_resume_is_idempotent(self):
+ model = self._network()
+ machine = FakeMemoryMachine()
+ model.switch("init", pc=0)
+ _port_call(machine, recv=(9, 8, 0x1000, 0x808))
+ model.handle(machine, pc=0x4019B00)
+ machine.write_mem(0x2000, b"\xab\xcd\xef\x01")
+ model.switch("rm", pc=0)
+ _port_call(machine, send=(1, 8, 0x2000, 4), recv=(2, 11, 0x3000, 0x100),
+ wait=2)
+ model.handle(machine, pc=0x5AD507C)
+ model.stopped = None
+ model.switch("init", pc=0)
+ _port_call(machine, wait=SHUTTLE_ANY)
+ handled, _ = model.handle(machine, pc=0x4019DF4)
+ self.assertTrue(handled)
+ self.assertEqual(machine.x[16], 9)
+ machine.write_mem(0x1000, b"\x7f")
+ _port_call(machine, send=(9, 0, 0x1000, 1), wait=9)
+ handled, _ = model.handle(machine, pc=0x401829C)
+ self.assertTrue(handled)
+ model.stopped = None
+ model.switch("rm", pc=0)
+ _port_call(machine, send=(1, 8, 0x2000, 4), recv=(2, 11, 0x3000, 0x100),
+ wait=2)
+ handled, _ = model.handle(machine, pc=0x5AD507C)
+ self.assertTrue(handled)
+ self.assertEqual(machine.x[16], 2)
+ self.assertEqual(machine.read_mem(0x3000, 1), b"\x7f")
+ # the resumed receive did not re-post a stale entry
+ self.assertNotIn(2, model.task_recvs["rm"])
+ self.assertEqual(model.cross_task_payloads[-1],
+ {"port": 0, "size": 1, "from": "init", "to": "rm",
+ "kind": "reply"})
+
+ def test_task_state_query_serves_the_modelled_trap_state(self):
+ model = self._network()
+ machine = FakeMemoryMachine()
+ model.switch("init", pc=0)
+ model.host_kick()
+ _port_call(machine, recv=(10, 6, 0, 0))
+ model.handle(machine, pc=0x4018038)
+ machine.set(5, INTERRUPT_ARM_EXTERNAL)
+ machine.set(10, 0xFF)
+ model.handle(machine, pc=0x4018054)
+ _port_call(machine, wait=SHUTTLE_ANY)
+ model.handle(machine, pc=0x4019DF4)
+ machine.set(5, 6)
+ machine.set(10, 1)
+ machine.set(11, 0x4000)
+ handled, result = model.handle(machine, pc=0x401A46C)
+ self.assertTrue(handled)
+ self.assertEqual(result, 0)
+ state = machine.read_mem(0x4000, 288)
+ self.assertEqual(int.from_bytes(state[256:264], "little"), SPURIOUS_XEPC)
+ self.assertEqual(int.from_bytes(state[264:272], "little"),
+ EXTERNAL_INTERRUPT_XCAUSE)
+ self.assertEqual(model.queries[-1]["task_index"], 1)
+
+ def test_memory_query_and_shuttle_reset_are_services(self):
+ model = self._network()
+ machine = FakeMemoryMachine()
+ machine.set(5, 5)
+ machine.set(10, 0xFF)
+ machine.set(11, 0x1000)
+ handled, result = model.handle(machine, pc=0x5AC44FC)
+ self.assertTrue(handled)
+ self.assertEqual(result, 0)
+ self.assertEqual(machine.x[12], 0x1000)
+ self.assertEqual(machine.x[14], 1)
+ machine.set(5, 7)
+ machine.set(10, 1)
+ handled, result = model.handle(machine, pc=0x5AB7D8C)
+ self.assertTrue(handled)
+ self.assertEqual(result, 0)
+ self.assertEqual([s["service"] for s in model.services],
+ ["memory_query", "shuttle_reset"])
+
+ def test_network_state_is_byte_identical_across_runs(self):
+ def scenario():
+ model = self._network()
+ machine = FakeMemoryMachine()
+ model.switch("init", pc=0)
+ _port_call(machine, recv=(9, 8, 0x1000, 0x808))
+ model.handle(machine, pc=0x4019B00)
+ model.switch("rm", pc=0)
+ _port_call(machine, send=(1, 8, 0x2000, 4),
+ recv=(2, 11, 0x3000, 0x100), wait=2)
+ model.handle(machine, pc=0x5AD507C)
+ return model.state()
+
+ first = scenario()
+ second = scenario()
+ self.assertEqual(json.dumps(first, sort_keys=True),
+ json.dumps(second, sort_keys=True))
+
+
if __name__ == "__main__":
unittest.main()
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_msgq_attach.py b/debug/risc-v-gsp-rm-vm/tests/test_msgq_attach.py
index 749157e..90643e9 100644
--- a/debug/risc-v-gsp-rm-vm/tests/test_msgq_attach.py
+++ b/debug/risc-v-gsp-rm-vm/tests/test_msgq_attach.py
@@ -43,7 +43,9 @@ from sim.msgq_attach import ( # noqa: E402
RingGeometry,
assemble_report,
classify_ring_access,
+ image_tasks,
missing_semantic_after_handoff,
+ missing_semantic_after_second_task,
ring_geometry_from_host,
run_attach,
)
@@ -236,6 +238,69 @@ class ReportTests(unittest.TestCase):
max_cycles=MAX_RUN_CYCLES + 1)
with self.assertRaises(MsgqAttachError):
run_attach(image="/nonexistent/gsp-image.bin", max_cycles=True)
+ with self.assertRaises(MsgqAttachError):
+ run_attach(image="/nonexistent/gsp-image.bin",
+ second_task_budget=-1)
+ with self.assertRaises(MsgqAttachError):
+ run_attach(image="/nonexistent/gsp-image.bin",
+ second_task_budget=MAX_RUN_CYCLES + 1)
+
+
+class TaskTableTests(unittest.TestCase):
+ """Data-driven task discovery from the real image section table."""
+
+ @unittest.skipUnless(os.path.isfile(DEFAULT_IMAGE), "real GSP firmware absent")
+ def test_two_tasks_are_discovered_with_entries_and_stacks(self):
+ doc = image_tasks(DEFAULT_IMAGE)
+ self.assertEqual(doc["format"], "greeneye-image-tasks/1")
+ names = [task["name"] for task in doc["tasks"]]
+ self.assertEqual(names, ["init", "rm"])
+ for task in doc["tasks"]:
+ self.assertGreaterEqual(task["entry"], task["text_base"])
+ self.assertLess(task["entry"],
+ task["text_base"] + task["text_size"])
+ self.assertGreater(task["stack_top"], task["stack_base"])
+ self.assertGreater(task["text_size"], 0)
+ rm = doc["tasks"][1]
+ self.assertGreater(rm["text_size"], doc["tasks"][0]["text_size"])
+
+ def test_absent_image_yields_an_empty_table(self):
+ doc = image_tasks("/nonexistent/image.bin")
+ self.assertEqual(doc, {"format": "greeneye-image-tasks/1", "tasks": []})
+
+ def test_second_task_missing_semantic_is_precise(self):
+ doc = image_tasks(DEFAULT_IMAGE) if os.path.isfile(DEFAULT_IMAGE) \
+ else {"format": "greeneye-image-tasks/1",
+ "tasks": [{"name": "init"}, {"name": "rm"}]}
+ missing = missing_semantic_after_second_task(
+ tasks_doc=doc,
+ schedule=[{"task": "init", "reason": "port-wait-blocks"},
+ {"task": "rm", "reason": "scheduled"}],
+ frontier={"task": "rm", "reason": "second-task-slice-budget"},
+ polls=[])
+ self.assertEqual(missing["stage"], "second-task-frontier")
+ self.assertEqual(missing["polls"], 0)
+ self.assertEqual(missing["second_task"], "rm")
+ self.assertGreaterEqual(len(missing["missing"]), 3)
+ self.assertTrue(missing["next_step"])
+
+ def test_report_carries_tasks_and_schedule_additively(self):
+ report = assemble_report(
+ image=_image(), profile=_profile(), boot=_boot(True), attach={},
+ doorbell={"requested": True}, polls=[], divergence=None,
+ missing_mmio={"count": 0, "first": None},
+ determinism={"instructions": 1},
+ tasks={"format": "greeneye-image-tasks/1",
+ "tasks": [{"name": "init"}, {"name": "rm"}]},
+ schedule=[{"task": "rm", "reason": "scheduled"}],
+ )
+ self.assertEqual([task["name"] for task in report["tasks"]["tasks"]],
+ ["init", "rm"])
+ self.assertEqual(report["schedule"][0]["task"], "rm")
+ # the schema tag and the original keys are untouched
+ self.assertEqual(report["format"], REPORT_FORMAT)
+ self.assertIn("first_poll", report)
+ self.assertIn("missing_semantic", report)
class PortModelReportTests(unittest.TestCase):
@@ -285,12 +350,12 @@ class ImageTests(unittest.TestCase):
"""Bounded real-firmware run: deterministic, honest outcome only.
Limitation: the budget is deliberately small (600k instructions; boot-to-B3
- needs ~474k). If the firmware needs more, the report is expected to take
- the honest ``not-reached`` path with a non-empty divergence reason -- this
- test accepts either outcome and asserts only determinism, never success.
- With the default port model the run advances past the host wake to the
- external-interrupt dispatch and reports the exact unmodelled semantic
- (``task-state-query``); a fabricated poll is never accepted.
+ needs ~474k). The scheduled second task (the image-discovered RM task)
+ completes its boot port handshake inside the bounded slice and then runs
+ its own start-up past the cap, so the report is expected to take the
+ honest ``wait-reached-no-poll`` frontier path -- this test accepts any of
+ the allowed outcomes and asserts determinism, never success. A fabricated
+ poll is never accepted.
"""
def test_bounded_run_is_deterministic_and_honest(self):
@@ -306,6 +371,12 @@ class ImageTests(unittest.TestCase):
self.assertGreater(report["image"]["size"], 0)
self.assertEqual(len(report["image"]["sha256"]), 64)
self.assertTrue(report["port_model"]["enabled"])
+ self.assertEqual(report["port_model"]["format"],
+ "greeneye-msgq-port-network/1")
+ self.assertEqual([task["name"] for task in report["tasks"]["tasks"]],
+ ["init", "rm"])
+ self.assertTrue(report["schedule"])
+ self.assertIn("rm", [step.get("task") for step in report["schedule"]])
if not report["boot"]["message_loop_wait_reached"]:
self.assertIsNotNone(report["divergence"])
self.assertTrue(report["divergence"]["reason"])
@@ -313,17 +384,22 @@ class ImageTests(unittest.TestCase):
self.assertTrue(report["first_poll"]["in_ring"])
self.assertTrue(report["first_poll"]["kind"] != "other")
divergence = report["divergence"]
- if divergence and divergence.get("kind") == "task-state-query":
- # The port model delivered the doorbell wake (extintr shuttle
- # 10) and stopped at the libos trap/report path; the missing
- # semantic is named, and no poll is claimed.
+ if divergence and divergence.get("kind") == "second-task-frontier":
+ # The doorbell wake was consumed by the init task and the
+ # discovered RM task completed the boot handshake before its
+ # start-up ran past the bounded slice; the exact frontier is
+ # named and no poll is claimed.
self.assertIsNone(report["first_poll"])
self.assertIsNotNone(report["missing_semantic"])
self.assertEqual(report["missing_semantic"]["stage"],
- "after-extintr-handoff")
+ "second-task-frontier")
completed = [wait.get("completed")
for wait in report["port_model"].get("waits", [])]
self.assertIn(10, completed)
+ payloads = report["port_model"].get("cross_task_payloads", [])
+ self.assertTrue(any(item.get("from") == "rm"
+ and item.get("to") == "init"
+ for item in payloads))
self.assertEqual(first["boot"]["message_loop_wait_reached"],
second["boot"]["message_loop_wait_reached"])
self.assertEqual(first["boot"]["instructions_executed"],
@@ -334,9 +410,25 @@ class ImageTests(unittest.TestCase):
self.assertEqual(first["continuation"], second["continuation"])
self.assertEqual(first["port_model"], second["port_model"])
self.assertEqual(first["missing_semantic"], second["missing_semantic"])
+ self.assertEqual(first["tasks"], second["tasks"])
+ self.assertEqual(first["schedule"], second["schedule"])
self.assertEqual(first["determinism"]["report_sha256"],
second["determinism"]["report_sha256"])
+ def test_single_task_mode_keeps_the_handoff_classification(self):
+ report = run_attach(image=DEFAULT_IMAGE, max_cycles=SMALL_BUDGET,
+ doorbell=True, second_task=False)
+ self.assertIn(report["outcome"],
+ ("not-reached", "wait-reached-no-poll",
+ "wait-reached-poll-classified"))
+ self.assertEqual(report["port_model"]["format"], "greeneye-msgq-port/1")
+ self.assertFalse(report["schedule"])
+ divergence = report["divergence"]
+ if divergence and divergence.get("kind") == "task-state-query":
+ self.assertIsNone(report["first_poll"])
+ self.assertEqual(report["missing_semantic"]["stage"],
+ "after-extintr-handoff")
+
if __name__ == "__main__":
unittest.main()
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_state_decode.py b/debug/risc-v-gsp-rm-vm/tests/test_state_decode.py
index 5c05ccd..3485ced 100644
--- a/debug/risc-v-gsp-rm-vm/tests/test_state_decode.py
+++ b/debug/risc-v-gsp-rm-vm/tests/test_state_decode.py
@@ -1,9 +1,10 @@
"""Tests for the engine-visible state decoder (harness/state_decode.py).
-Offline only: text/hexdump/binary/snapshot-JSON parsing, the bundled
-INSTANCE/RAMFC field table, map override/extension, decode/diff reports and
-the CLI. The real-evidence test parses a frozen live ladder trace and skips
-when the backup is absent. No hardware, no module, no guest, no network.
+Offline only: text/hexdump/binary/snapshot-JSON/aggregate-group parsing, the
+bundled INSTANCE/RAMFC field table, map override/extension, decode/diff
+reports and the CLI. The real-evidence tests parse frozen live ladder traces
+and an aggregate GSP physical-state dump, and skip when the backups are
+absent. No hardware, no module, no guest, no network.
Run::
@@ -26,6 +27,7 @@ if TOOL_ROOT not in sys.path:
sys.path.insert(0, TOOL_ROOT)
from harness.state_decode import ( # noqa: E402
+ AGGREGATE_FORMAT,
DECODE_FORMAT,
DEMO_FORMAT,
DIFF_FORMAT,
@@ -38,6 +40,7 @@ from harness.state_decode import ( # noqa: E402
load_input,
load_map,
main,
+ render_diff,
run_demo,
)
@@ -45,6 +48,17 @@ EVIDENCE = os.environ.get("GREENEYE_STATE_DECODE_EVIDENCE") or os.path.normpath(
os.path.join(TOOL_ROOT, "..", "..", "backups",
"evidence-20261002-victory", "06-ladder-m12.txt"))
+# Our own evidence artifact directory (not a vendor path). The aggregate
+# dump is one group per line: ``stage: primed=1 kicked=1 tx_seq_num=106``.
+AGGREGATE_EVIDENCE = os.environ.get(
+ "GREENEYE_STATE_DECODE_EVIDENCE_AGG") or os.path.normpath(
+ os.path.join(TOOL_ROOT, "..", "..", "backups",
+ "evidence-20261002-victory", "20-gsp-phys-state-m26.txt"))
+AGGREGATE_EVIDENCE_PEER = os.environ.get(
+ "GREENEYE_STATE_DECODE_EVIDENCE_AGG_PEER") or os.path.normpath(
+ os.path.join(TOOL_ROOT, "..", "..", "backups",
+ "evidence-20261002-victory", "20-gsp-phys-state-m29.txt"))
+
def _run_main(argv: list[str]) -> tuple[int, str, str]:
out, err = io.StringIO(), io.StringIO()
@@ -197,6 +211,147 @@ class ParsingTests(unittest.TestCase):
load_input(_spec(path, binary=True))
+class AggregateParsingTests(unittest.TestCase):
+ def _decode(self, tmp: str, name: str, text: str) -> dict:
+ entries, source = load_map(None)
+ path = _write(os.path.join(tmp, name), text)
+ return decode_specs([_spec(path)], entries, source)
+
+ def test_aggregate_line_decodes_types_and_preserves_raw(self):
+ text = "\n".join([
+ "stage: primed=1 kicked=1 tx_seq_num=106 variant=V6",
+ "ctx: pa=0x1f4000000 size=0x133000 last_ret=-5 "
+ "userd=sysmem (host-writable)",
+ ])
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write(os.path.join(tmp, "agg.txt"), text)
+ document = load_input(_spec(path))
+ self.assertEqual(document["format"], AGGREGATE_FORMAT)
+ self.assertEqual([chunk["label"] for chunk in document["chunks"]],
+ ["PHYS_stage", "PHYS_ctx"])
+ stage = document["chunks"][0]
+ self.assertEqual(stage["format"], AGGREGATE_FORMAT)
+ self.assertIn("stage: primed=1", stage["raw"])
+ entries, source = load_map(None)
+ report = decode_specs([_spec(path)], entries, source)
+ chunks = {chunk["label"]: chunk for chunk in report["chunks"]}
+ stage = chunks["PHYS_stage"]
+ self.assertEqual(stage["input_format"], AGGREGATE_FORMAT)
+ self.assertEqual(stage["word_count"], 0)
+ self.assertEqual(stage["kv_count"], 4)
+ self.assertEqual(stage["raw"],
+ "stage: primed=1 kicked=1 tx_seq_num=106 "
+ "variant=V6")
+ fields = {field["name"]: field for field in stage["fields"]}
+ self.assertEqual(fields["primed"]["value"], 1)
+ self.assertTrue(fields["primed"]["numeric"])
+ self.assertEqual(fields["tx_seq_num"]["value"], 106)
+ self.assertEqual(fields["variant"]["value"], "V6")
+ self.assertFalse(fields["variant"]["numeric"])
+ self.assertEqual(fields["variant"]["raw"], "V6")
+ self.assertEqual(fields["variant"]["kind"], "kv")
+ self.assertIsNone(fields["variant"]["offset"])
+ ctx = {field["name"]: field for field in chunks["PHYS_ctx"]["fields"]}
+ self.assertEqual(ctx["pa"]["value"], 0x1F4000000)
+ self.assertEqual(ctx["size"]["value"], 0x133000)
+ self.assertEqual(ctx["last_ret"]["value"], -5)
+ self.assertEqual(ctx["userd"]["value"], "sysmem")
+ self.assertNotIn("host-writable", ctx)
+
+ def test_aggregate_missing_value_and_unparsed_lines(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ report = self._decode(tmp, "agg.txt", "\n".join([
+ "banner line that must be ignored",
+ "status_ring: writePtr slot=29; last produced elements "
+ "(max 8):",
+ "peer: host status readPtr @cmd+0x20 = 8",
+ "empty: a= b=1",
+ ]))
+ self.assertEqual([chunk["label"] for chunk in report["chunks"]],
+ ["PHYS_empty"])
+ fields = {field["name"]: field for field in
+ report["chunks"][0]["fields"]}
+ self.assertEqual(fields["a"]["value"], "")
+ self.assertEqual(fields["a"]["raw"], "")
+ self.assertFalse(fields["a"]["numeric"])
+ self.assertEqual(fields["b"]["value"], 1)
+
+ def test_aggregate_single_kv_group_is_captured(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ report = self._decode(tmp, "agg.txt", "guest_ram: active=0\n")
+ self.assertEqual([chunk["label"] for chunk in report["chunks"]],
+ ["PHYS_guest_ram"])
+ field = report["chunks"][0]["fields"][0]
+ self.assertEqual(field["name"], "active")
+ self.assertEqual(field["value"], 0)
+
+ def test_aggregate_pipe_groups_and_duplicate_keys(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ report = self._decode(tmp, "agg.txt", "\n".join([
+ "rm_call_userd: userd=sysmem (host-writable) | "
+ "rm_call_pdb: vaBase=0x100000 vaGpfifo=0x200000",
+ "queue_dma: shared=0x1a86b6000 ptes=129 | "
+ "radix3=0x105370000 fmc=0x142d25000",
+ "guest_ram: active=1 gpa=0x0 gpa=0x100000000 "
+ "size=0x80000000 size=0x80000000",
+ ]))
+ self.assertEqual([chunk["label"] for chunk in report["chunks"]], [
+ "PHYS_guest_ram", "PHYS_queue_dma", "PHYS_rm_call_pdb",
+ "PHYS_rm_call_userd",
+ ])
+ chunks = {chunk["label"]: chunk for chunk in report["chunks"]}
+ userd = {field["name"]: field for field in
+ chunks["PHYS_rm_call_userd"]["fields"]}
+ self.assertEqual(userd["userd"]["value"], "sysmem")
+ pdb = [field["name"] for field in chunks["PHYS_rm_call_pdb"]["fields"]]
+ self.assertEqual(pdb, ["vaBase", "vaGpfifo"])
+ queue = [field["name"] for field in chunks["PHYS_queue_dma"]["fields"]]
+ self.assertEqual(queue, ["shared", "ptes", "radix3", "fmc"])
+ gpa = [field for field in chunks["PHYS_guest_ram"]["fields"]
+ if field["name"] == "gpa"]
+ self.assertEqual([field["index"] for field in gpa], [0, 1])
+ self.assertEqual([field["value"] for field in gpa],
+ [0, 0x100000000])
+
+ def test_mixed_word_and_aggregate_lines(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write(os.path.join(tmp, "mixed.txt"), "\n".join([
+ "INSTANCE[0x10]=0xface",
+ "stage: a=1 b=2",
+ ]))
+ document = load_input(_spec(path))
+ self.assertEqual(document["format"], "text")
+ self.assertEqual([chunk["label"] for chunk in document["chunks"]],
+ ["INSTANCE", "PHYS_stage"])
+
+ def test_unstructured_text_still_rejected(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write(os.path.join(tmp, "none.txt"), "\n".join([
+ "status_ring: writePtr slot=29; last produced elements:",
+ "peer: host status readPtr @cmd+0x20 = 8",
+ ]))
+ with self.assertRaises(StateDecodeError):
+ load_input(_spec(path))
+
+ def test_aggregate_decode_is_deterministic(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ text = "stage: a=1 b=0x10 c=up\n"
+ first = self._decode(tmp, "one.txt", text)
+ second = self._decode(tmp, "two.txt", text)
+ stripped = [
+ {key: value for key, value in chunk.items()
+ if key != "source"}
+ for chunk in first["chunks"]
+ ]
+ other = [
+ {key: value for key, value in chunk.items()
+ if key != "source"}
+ for chunk in second["chunks"]
+ ]
+ self.assertEqual(json.dumps(stripped, sort_keys=True),
+ json.dumps(other, sort_keys=True))
+
+
class DecodeTests(unittest.TestCase):
def _ramfc_target(self, word48: int) -> dict:
report = _report_from_chunks([
@@ -394,6 +549,132 @@ class DiffTests(unittest.TestCase):
self.assertEqual(report["summary"]["removed"], 1)
+class AggregateDiffTests(unittest.TestCase):
+ def _pair(self, tmp: str, left_text: str, right_text: str) -> tuple:
+ entries, source = load_map(None)
+ left = _write(os.path.join(tmp, "left.txt"), left_text)
+ right = _write(os.path.join(tmp, "right.txt"), right_text)
+ return (
+ decode_specs([_spec(left)], entries, source),
+ decode_specs([_spec(right)], entries, source),
+ )
+
+ def test_diff_classifies_added_removed_changed_kvs(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ left, right = self._pair(
+ tmp,
+ "stage: a=1 b=2 gone=1 variant=V6\n",
+ "stage: a=1 b=3 variant=V7 extra=4\n")
+ report = diff_reports(left, right)
+ self.assertEqual(report["format"], DIFF_FORMAT)
+ self.assertFalse(report["identical"])
+ chunk = report["chunks"][0]
+ self.assertEqual(chunk["label"], "PHYS_stage")
+ self.assertEqual(chunk["status"], "changed")
+ self.assertEqual(chunk["added"], [])
+ self.assertEqual(chunk["removed"], [])
+ self.assertEqual(chunk["changed"], [])
+ rows = {row["name"]: row for row in chunk["changed_fields"]}
+ self.assertEqual(rows["b"]["status"], "changed")
+ self.assertEqual((rows["b"]["old"], rows["b"]["new"]), (2, 3))
+ self.assertEqual(rows["variant"]["status"], "changed")
+ self.assertEqual((rows["variant"]["old"], rows["variant"]["new"]),
+ ("V6", "V7"))
+ self.assertEqual(rows["extra"]["status"], "added")
+ self.assertIsNone(rows["extra"]["old"])
+ self.assertEqual(rows["extra"]["new"], 4)
+ self.assertEqual(rows["gone"]["status"], "removed")
+ self.assertEqual(rows["gone"]["old"], 1)
+ self.assertIsNone(rows["gone"]["new"])
+ self.assertEqual([row["name"] for row in chunk["fields_added"]],
+ ["extra"])
+ self.assertEqual([row["name"] for row in chunk["fields_removed"]],
+ ["gone"])
+ self.assertEqual([row["name"] for row in chunk["fields_changed"]],
+ ["b", "variant"])
+ self.assertEqual(report["summary"]["fields_added"], 1)
+ self.assertEqual(report["summary"]["fields_removed"], 1)
+ self.assertEqual(report["summary"]["fields_changed"], 2)
+ self.assertEqual(report["summary"]["changed_fields"], 4)
+ again = diff_reports(left, right)
+ self.assertEqual(json.dumps(report, sort_keys=True),
+ json.dumps(again, sort_keys=True))
+
+ def test_diff_aggregate_identical_and_one_sided(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ left, right = self._pair(tmp, "stage: a=1 b=up\n",
+ "stage: a=1 b=up\n")
+ report = diff_reports(left, right)
+ self.assertTrue(report["identical"])
+ self.assertEqual(report["summary"]["fields_added"], 0)
+ self.assertEqual(report["summary"]["fields_removed"], 0)
+ self.assertEqual(report["summary"]["fields_changed"], 0)
+ only_left, only_right = self._pair(
+ tmp, "stage: a=1 b=2\n", "ctx: x=1\n")
+ report = diff_reports(only_left, only_right)
+ labels = {chunk["label"]: chunk for chunk in report["chunks"]}
+ self.assertEqual(labels["PHYS_stage"]["status"], "left_only")
+ self.assertEqual(
+ [row["name"] for row in labels["PHYS_stage"]["fields_removed"]],
+ ["a", "b"])
+ self.assertEqual(labels["PHYS_ctx"]["status"], "right_only")
+ self.assertEqual(
+ [row["name"] for row in labels["PHYS_ctx"]["fields_added"]],
+ ["x"])
+
+ def test_diff_aggregate_duplicate_key_occurrence(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ left, right = self._pair(
+ tmp, "mem: size=0x10 size=0x20\n",
+ "mem: size=0x10 size=0x30\n")
+ chunk = diff_reports(left, right)["chunks"][0]
+ self.assertEqual(len(chunk["fields_changed"]), 1)
+ row = chunk["fields_changed"][0]
+ self.assertEqual(row["name"], "size")
+ self.assertEqual(row["index"], 1)
+ self.assertEqual((row["old"], row["new"]), (0x20, 0x30))
+
+ def test_cli_diff_aggregate_exit_codes_and_kv_rendering(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ left = _write(os.path.join(tmp, "left.txt"),
+ "stage: a=1 b=2 state=up\n")
+ right = _write(os.path.join(tmp, "right.txt"),
+ "stage: a=1 b=3 state=up\n")
+ rc, out, err = _run_main(["diff", left, right])
+ self.assertEqual(rc, 3, err)
+ self.assertIn("PHYS_stage", out)
+ self.assertIn("kv b changed: 2 -> 3", out)
+ rc, out, err = _run_main(["diff", left, left])
+ self.assertEqual(rc, 0, err)
+ self.assertIn("identical=True", out)
+
+ def test_render_diff_aggregate_handles_added_and_removed(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ left, right = self._pair(tmp, "stage: gone=1\n",
+ "stage: extra=2\n")
+ text = render_diff(diff_reports(left, right))
+ self.assertIn("kv extra added: - -> 2", text)
+ self.assertIn("kv gone removed: 1 -> -", text)
+
+ def test_cli_decode_aggregate_writes_report(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write(os.path.join(tmp, "agg.txt"),
+ "stage: primed=1 variant=V6\n")
+ out_path = os.path.join(tmp, "agg.json")
+ rc, out, err = _run_main(["decode", "--input", path,
+ "--json", out_path])
+ self.assertEqual(rc, 0, err)
+ self.assertIn("PHYS_stage", out)
+ self.assertIn("(aggregate)", out)
+ with open(out_path, encoding="utf-8") as handle:
+ report = json.load(handle)
+ self.assertEqual(report["format"], DECODE_FORMAT)
+ self.assertEqual(report["sources"][0]["format"], AGGREGATE_FORMAT)
+ self.assertEqual(report["chunks"][0]["input_format"],
+ AGGREGATE_FORMAT)
+ self.assertEqual(report["chunks"][0]["fields"][1]["value"], "V6")
+
+
class CliAndDemoTests(unittest.TestCase):
def test_cli_decode_prints_table_and_writes_json(self):
with tempfile.TemporaryDirectory() as tmp:
@@ -514,5 +795,47 @@ class EvidenceTests(unittest.TestCase):
self.assertEqual(fields["MAGIC_0FACE"]["raw"], "0x0000face")
+@unittest.skipUnless(os.path.isfile(AGGREGATE_EVIDENCE),
+ "aggregate GSP physical-state evidence absent")
+class AggregateEvidenceTests(unittest.TestCase):
+ """Frozen GSP physical-state dumps from our own evidence directory.
+
+ Override with GREENEYE_STATE_DECODE_EVIDENCE_AGG (and ..._PEER); skipped
+ when the files are absent. No hardware, no module, no guest, no network.
+ """
+
+ def test_aggregate_evidence_decodes_groups(self):
+ entries, source = load_map(None)
+ report = decode_specs([_spec(AGGREGATE_EVIDENCE)], entries, source)
+ self.assertGreater(len(report["chunks"]), 0)
+ self.assertEqual(report["sources"][0]["format"], AGGREGATE_FORMAT)
+ chunks = {chunk["label"]: chunk for chunk in report["chunks"]}
+ for label in ("PHYS_stage", "PHYS_rm_call", "PHYS_guest_ram",
+ "PHYS_rm_call_ctx"):
+ self.assertIn(label, chunks)
+ stage = {field["name"]: field for field in
+ chunks["PHYS_stage"]["fields"]}
+ self.assertEqual(stage["primed"]["value"], 1)
+ self.assertTrue(stage["tx_seq_num"]["numeric"])
+
+ def test_aggregate_evidence_diff_peer_shows_changed_kvs(self):
+ if not os.path.isfile(AGGREGATE_EVIDENCE_PEER):
+ self.skipTest("aggregate peer evidence absent")
+ entries, source = load_map(None)
+ left = decode_specs([_spec(AGGREGATE_EVIDENCE)], entries, source)
+ right = decode_specs([_spec(AGGREGATE_EVIDENCE_PEER)], entries, source)
+ report = diff_reports(left, right)
+ self.assertFalse(report["identical"])
+ self.assertGreater(report["summary"]["fields_changed"], 0)
+ self.assertEqual(report["summary"]["changed_words"], 0)
+ chunks = {chunk["label"]: chunk for chunk in report["chunks"]}
+ rows = {row["name"]: row for row in
+ chunks["PHYS_guest_ram"]["changed_fields"]}
+ self.assertEqual(rows["active"]["status"], "changed")
+ self.assertEqual((rows["active"]["old"], rows["active"]["new"]),
+ (1, 0))
+ render_diff(report)
+
+
if __name__ == "__main__":
unittest.main()