summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py397
1 files changed, 397 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py b/debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py
new file mode 100644
index 0000000..9211c81
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/tests/test_gmmu_oracle.py
@@ -0,0 +1,397 @@
+"""Tests for the GMMU/FBIF oracle comparison (harness/gmmu_oracle.py, docs/44).
+
+All fixtures are synthetic and written to temporary directories: no oracle
+capture, no hardware, no simulator run, no QEMU, no network. The comparison
+is offline file-in/file-out. Tests that would need real inputs are written
+to assert the clean error path instead.
+
+Run::
+
+ python3 -m unittest tests.test_gmmu_oracle -v
+"""
+from __future__ import annotations
+
+import contextlib
+import io
+import json
+import os
+import sys
+import tempfile
+import unittest
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOOL_ROOT = os.path.abspath(os.path.join(HERE, ".."))
+if TOOL_ROOT not in sys.path:
+ sys.path.insert(0, TOOL_ROOT)
+
+from harness.gmmu_debug import GmmuInputError, TRACE_FORMAT # noqa: E402
+from harness.gmmu_oracle import ( # noqa: E402
+ ORACLE_FORMAT,
+ REPORT_FORMAT,
+ compare,
+ load_oracle,
+ load_sim_trace,
+ main,
+ print_capture_hints,
+ run_compare,
+)
+
+
+def _write_json(path: str, payload) -> str:
+ with open(path, "w", encoding="utf-8") as fh:
+ json.dump(payload, fh)
+ return path
+
+
+def _oracle_items() -> list[dict]:
+ return [
+ {
+ "name": "FBIF_CTL",
+ "window": "falcon",
+ "offset": "0x624",
+ "value": "0x1",
+ "source": "rm-readback",
+ "captured_at": "2026-10-03",
+ },
+ {
+ "name": "GMMU_PDB_LO",
+ "window": "gmmu",
+ "offset": "0x0",
+ "value": "0x400000",
+ "source": "rm-readback",
+ "captured_at": "2026-10-03",
+ },
+ ]
+
+
+def _sim_events() -> list[dict]:
+ return [
+ {
+ "seq": 0,
+ "pc": "0x4002040",
+ "kind": "write",
+ "window": "falcon",
+ "offset": "0x624",
+ "name": "FBIF_CTL",
+ "size": 4,
+ "value": "0x1",
+ },
+ {
+ "seq": 1,
+ "pc": "0x4001010",
+ "kind": "write",
+ "window": "gmmu",
+ "offset": "0x0",
+ "name": "GMMU_PDB_LO",
+ "size": 4,
+ "value": "0x400000",
+ },
+ ]
+
+
+def _run_main(argv: list[str]) -> tuple[int, str, str]:
+ out, err = io.StringIO(), io.StringIO()
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
+ rc = main(argv)
+ return rc, out.getvalue(), err.getvalue()
+
+
+class IngestTests(unittest.TestCase):
+ def test_oracle_object_and_bare_list(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ tagged = _write_json(
+ os.path.join(tmp, "o1.json"),
+ {"format": ORACLE_FORMAT, "captures": _oracle_items()},
+ )
+ bare = _write_json(os.path.join(tmp, "o2.json"), _oracle_items())
+ self.assertEqual(load_oracle(tagged).format, ORACLE_FORMAT)
+ self.assertEqual(load_oracle(bare).format, "unlabeled-list")
+ self.assertEqual(len(load_oracle(tagged).items), 2)
+ self.assertEqual(load_oracle(tagged).items[0].value, 1)
+ self.assertEqual(load_oracle(tagged).items[1].offset, 0)
+
+ def test_oracle_wrong_format_is_clean_error(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write_json(
+ os.path.join(tmp, "o.json"), {"format": "something-else", "captures": []}
+ )
+ with self.assertRaises(GmmuInputError):
+ load_oracle(path)
+
+ def test_oracle_missing_file_is_clean_error(self):
+ with self.assertRaises(GmmuInputError):
+ load_oracle("/does/not/exist.json")
+
+ def test_trace_aliases(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ path = _write_json(
+ os.path.join(tmp, "t.json"),
+ {
+ "format": TRACE_FORMAT,
+ "events": [
+ {
+ "pc": 0x4002040,
+ "rw": "w",
+ "block": "falcon",
+ "reg_offset": 0x624,
+ "register": "FBIF_CTL",
+ "value": 1,
+ }
+ ],
+ },
+ )
+ trace = load_sim_trace(path)
+ obs = trace.observations[0]
+ self.assertEqual(obs.kind, "write")
+ self.assertEqual(obs.window, "falcon")
+ self.assertEqual(obs.offset, 0x624)
+ self.assertEqual(obs.name, "FBIF_CTL")
+ self.assertEqual(obs.value, 1)
+
+ def test_trace_missing_file_is_clean_error(self):
+ with self.assertRaises(GmmuInputError):
+ load_sim_trace("/does/not/exist.json")
+
+
+class ComparisonTests(unittest.TestCase):
+ def _compare(self, items, events):
+ with tempfile.TemporaryDirectory() as tmp:
+ oracle_path = _write_json(
+ os.path.join(tmp, "oracle.json"),
+ {"format": ORACLE_FORMAT, "captures": items},
+ )
+ trace_path = _write_json(
+ os.path.join(tmp, "trace.json"), {"format": TRACE_FORMAT, "events": events}
+ )
+ return compare(load_oracle(oracle_path), load_sim_trace(trace_path))
+
+ def test_full_agreement(self):
+ report = self._compare(_oracle_items(), _sim_events())
+ summary = report["summary"]
+ self.assertEqual(summary["oracle_items"], 2)
+ self.assertEqual(summary["compared"], 2)
+ self.assertEqual(summary["agreed"], 2)
+ self.assertEqual(summary["mismatched"], 0)
+ self.assertEqual(summary["oracle_only"], 0)
+ self.assertEqual(summary["simulator_only"], 0)
+ self.assertEqual(summary["agreement_pct"], 100.0)
+ self.assertEqual(summary["coverage_pct"], 100.0)
+ self.assertTrue(all(row["status"] == "agree" for row in report["rows"]))
+ self.assertTrue(all(row["fidelity"] == "observed vs modelled" for row in report["rows"]))
+
+ def test_mismatch_is_reported(self):
+ events = _sim_events()
+ events[0]["value"] = "0x2"
+ report = self._compare(_oracle_items(), events)
+ summary = report["summary"]
+ self.assertEqual(summary["mismatched"], 1)
+ self.assertEqual(summary["agreed"], 1)
+ self.assertEqual(summary["agreement_pct"], 50.0)
+ mismatch = next(row for row in report["rows"] if row["status"] == "mismatch")
+ self.assertEqual(mismatch["oracle_value"], 1)
+ self.assertEqual(mismatch["sim_value"], 2)
+
+ def test_missing_simulator_observation(self):
+ report = self._compare(_oracle_items(), _sim_events()[:1])
+ summary = report["summary"]
+ self.assertEqual(summary["oracle_only"], 1)
+ self.assertEqual(summary["coverage_pct"], 50.0)
+ self.assertEqual(len(report["unmatched_oracle"]), 1)
+ self.assertEqual(report["unmatched_oracle"][0]["register"], "GMMU_PDB_LO")
+ oracle_only = next(row for row in report["rows"] if row["status"] == "oracle-only")
+ self.assertIn("observed", oracle_only["fidelity"])
+
+ def test_missing_oracle_counterpart(self):
+ extra = {
+ "seq": 9,
+ "pc": "0x4003010",
+ "kind": "write",
+ "window": "gmmu",
+ "offset": "0x200",
+ "name": "PTE_DATA",
+ "value": "0x1",
+ }
+ report = self._compare(_oracle_items(), _sim_events() + [extra])
+ self.assertEqual(report["summary"]["simulator_only"], 1)
+ self.assertEqual(len(report["unmatched_simulator"]), 1)
+ self.assertEqual(report["unmatched_simulator"][0]["register"], "PTE_DATA")
+ self.assertEqual(report["summary"]["simulator_match_pct"], 66.67)
+
+ def test_name_fallback_when_oracle_has_no_offset(self):
+ items = [{"name": "IRQSTAT", "window": "falcon", "value": "0x0", "source": "rm"}]
+ events = [
+ {
+ "seq": 0,
+ "pc": 0x4002050,
+ "kind": "read",
+ "window": "falcon",
+ "offset": "0x8",
+ "name": "IRQSTAT",
+ "value": "0x0",
+ }
+ ]
+ report = self._compare(items, events)
+ self.assertEqual(report["summary"]["agreed"], 1)
+ self.assertEqual(report["rows"][0]["matched_by"], "window-name")
+
+ def test_no_value_is_not_agreement(self):
+ items = [{"name": "FBIF_CTL", "window": "falcon", "offset": "0x624"}]
+ report = self._compare(items, _sim_events()[:1])
+ self.assertEqual(report["summary"]["no_value"], 1)
+ self.assertEqual(report["summary"]["agreement_pct"], None)
+
+ def test_g1_nested_document_compares(self):
+ document = {
+ "schema": TRACE_FORMAT,
+ "image_sha256": "ab" * 32,
+ "profile": "tu104-570.144",
+ "windows": [
+ {
+ "label": "falcon-observe",
+ "registers": [
+ {
+ "offset": "0x624",
+ "name": "FBIF_CTL",
+ "access": "rw",
+ "accesses": [
+ {"addr": "0x300110624", "offset": "0x624",
+ "size": 4, "rw": "w", "value": "0x1",
+ "pc": "0x4002040", "window": "falcon-observe",
+ "register": "FBIF_CTL", "served": False,
+ "pc_symbol": None},
+ ],
+ }
+ ],
+ }
+ ],
+ }
+ with tempfile.TemporaryDirectory() as tmp:
+ oracle_path = _write_json(
+ os.path.join(tmp, "oracle.json"),
+ {
+ "format": ORACLE_FORMAT,
+ "captures": [
+ {"name": "FBIF_CTL", "window": "falcon-observe",
+ "offset": "0x624", "value": "0x1", "source": "rm"}
+ ],
+ },
+ )
+ trace_path = _write_json(os.path.join(tmp, "trace.json"), document)
+ report = compare(load_oracle(oracle_path), load_sim_trace(trace_path))
+ self.assertEqual(report["summary"]["oracle_items"], 1)
+ self.assertEqual(report["summary"]["agreed"], 1)
+ self.assertEqual(report["inputs"]["simulator"]["format"], TRACE_FORMAT)
+
+ def test_empty_inputs(self):
+ report = self._compare([], [])
+ summary = report["summary"]
+ self.assertEqual(summary["oracle_items"], 0)
+ self.assertEqual(summary["compared"], 0)
+ self.assertEqual(summary["agreement_pct"], None)
+ self.assertEqual(summary["coverage_pct"], None)
+ self.assertEqual(report["rows"], [])
+ self.assertEqual(report["unmatched_simulator"], [])
+
+
+class SessionIngestTests(unittest.TestCase):
+ def test_session_directory_with_blobs_trace(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ session = os.path.join(tmp, "session")
+ blobs = os.path.join(session, "blobs")
+ os.makedirs(blobs)
+ _write_json(
+ os.path.join(session, "session.json"),
+ {"format": "greeneye-session/1", "meta": {}},
+ )
+ _write_json(
+ os.path.join(blobs, "gmmu-fbif-trace.json"),
+ {"format": TRACE_FORMAT, "events": _sim_events()},
+ )
+ trace = load_sim_trace(session)
+ self.assertEqual(len(trace.observations), 2)
+ self.assertEqual(trace.format, TRACE_FORMAT)
+
+ def test_session_without_trace_is_clean_error(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ with self.assertRaises(GmmuInputError):
+ load_sim_trace(tmp)
+
+
+class ReportArtifactTests(unittest.TestCase):
+ def test_run_compare_writes_json_and_markdown(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ oracle_path = _write_json(
+ os.path.join(tmp, "oracle.json"),
+ {"format": ORACLE_FORMAT, "captures": _oracle_items()},
+ )
+ trace_path = _write_json(
+ os.path.join(tmp, "trace.json"),
+ {"format": TRACE_FORMAT, "events": _sim_events()},
+ )
+ out = os.path.join(tmp, "out")
+ report = run_compare(oracle_path, trace_path, out)
+ self.assertEqual(report["format"], REPORT_FORMAT)
+ self.assertTrue(os.path.isfile(report["artifacts"]["report_json"]))
+ self.assertTrue(os.path.isfile(report["artifacts"]["report_md"]))
+ with open(report["artifacts"]["report_json"], "r", encoding="utf-8") as fh:
+ loaded = json.load(fh)
+ self.assertEqual(loaded["summary"]["agreed"], 2)
+ with open(report["artifacts"]["report_md"], "r", encoding="utf-8") as fh:
+ md = fh.read()
+ self.assertIn("oracle vs simulator report", md)
+ self.assertIn("No cycle/timing claim", md)
+ self.assertIn("Oracle items with no simulator observation", md)
+
+
+class CliTests(unittest.TestCase):
+ def test_print_capture_hints_executes_nothing(self):
+ out, err = io.StringIO(), io.StringIO()
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
+ print_capture_hints(out)
+ text = out.getvalue()
+ self.assertIn("SUPERVISED, READ-ONLY, HUMAN-RUN ONLY", text)
+ self.assertIn("executes NOTHING", text)
+ self.assertIn("RUNBOOK P1/P7", text)
+ self.assertEqual(err.getvalue(), "")
+
+ def test_print_capture_hints_cli(self):
+ rc, stdout, _stderr = _run_main(["--print-capture-hints"])
+ self.assertEqual(rc, 0)
+ self.assertIn("rmharness/harness.sh", stdout)
+ self.assertIn("gmmu_oracle.py compare", stdout)
+
+ def test_compare_cli_writes_report(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ oracle_path = _write_json(
+ os.path.join(tmp, "oracle.json"),
+ {"format": ORACLE_FORMAT, "captures": _oracle_items()},
+ )
+ trace_path = _write_json(
+ os.path.join(tmp, "trace.json"),
+ {"format": TRACE_FORMAT, "events": _sim_events()},
+ )
+ out = os.path.join(tmp, "out")
+ rc, stdout, _stderr = _run_main(
+ ["compare", "--oracle", oracle_path, "--sim", trace_path, "--out", out]
+ )
+ self.assertEqual(rc, 0)
+ payload = json.loads(stdout[stdout.index("{"):stdout.rindex("}") + 1])
+ self.assertEqual(payload["summary"]["agreed"], 2)
+ self.assertTrue(os.path.isfile(os.path.join(out, "gmmu-oracle-report.json")))
+
+ def test_compare_missing_inputs_exits_2(self):
+ with tempfile.TemporaryDirectory() as tmp:
+ rc, _stdout, stderr = _run_main(
+ ["compare", "--oracle", "/nope.json", "--sim", "/nope.json", "--out", tmp]
+ )
+ self.assertEqual(rc, 2)
+ self.assertIn("oracle not found", stderr)
+
+ def test_no_command_prints_help(self):
+ rc, stdout, _stderr = _run_main([])
+ self.assertEqual(rc, 2)
+ self.assertIn("usage", stdout.lower())
+
+
+if __name__ == "__main__":
+ unittest.main()