"""Differential harness: run one demo scenario on QEMU and native, then diff. python3 examples/scripts/sim_diff.py [--out DIR] [--keep] Both backends execute the same scripted profile: enter/leave hooks on every uproc RPC handler, a write watchpoint on ``gsp_ctrl->rpc_count``, and a fixed 0.3 s serviced run. The scenario is deterministic on both backends (the demo self-test finishes and call counts are fixed), so the hook/watch/breakpoint profiles must match exactly. Sessions are recorded under ``--out`` (or a temporary directory) and ``analyze.compare_sessions`` renders the delta report. Exit status: 0 when the profiles match (or QEMU is unavailable: skip), 1 when they diverge. """ from __future__ import annotations import argparse import json import os import shutil import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) import greeneye as ge from greeneye import analyze DEMO = ROOT / "firmware" / "uproc_demo" / "build" / "uproc_demo.elf" RPC_HANDLERS = ["rpc_ping", "rpc_echo", "rpc_fill", "rpc_sum", "rpc_state"] DURATION = 0.3 def run_scenario(backend: str, session_dir: str) -> dict: """Record one scripted run and return its canonical profile.""" session = ge.launch(kernel=str(DEMO), backend=backend, record=session_dir, name=f"diff-{backend}") events: list = [] watch_hits: list = [] try: for name in RPC_HANDLERS: session.hook( name, on_enter=lambda hit, n=name: events.append(["enter", n, hit.arg(0), hit.arg(1)]), on_leave=lambda hit, n=name: events.append(["leave", n, hit.retval]), ) ctrl = session.symbols.lookup("gsp_ctrl") session.watch(ctrl + 16, size=8, kind="w", callback=lambda hit: watch_hits.append(hit.addr)) session.run(duration=DURATION) uart = session.target.read_uart() bp_counts = [handle.hits for handle in session._bps.values()] finally: session.close() return { "hook_events": events, "watch_hits": watch_hits, "bp_counts": bp_counts, "uart_first_line": uart.splitlines()[0] if uart else "", } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) parser.add_argument("--out", help="directory for the two recorded sessions") parser.add_argument("--keep", action="store_true", help="keep a temporary --out directory") args = parser.parse_args(argv) if not DEMO.exists(): print(f"skip: demo firmware not built ({DEMO}); run `make demo`") return 0 if not shutil.which(os.environ.get("GREENEYE_QEMU", "qemu-system-riscv64")): print("skip: qemu-system-riscv64 not found; native profile not compared") return 0 out = args.out or tempfile.mkdtemp(prefix="greeneye-simdiff-") os.makedirs(out, exist_ok=True) qemu = run_scenario("qemu", os.path.join(out, "qemu")) native = run_scenario("native", os.path.join(out, "native")) report = analyze.compare_sessions( os.path.join(out, "qemu"), os.path.join(out, "native") ) if not args.out and not args.keep: shutil.rmtree(out, ignore_errors=True) location = "" else: location = out print(f"# QEMU vs native differential profile ({location})") print(json.dumps(qemu, sort_keys=True)) print(json.dumps(native, sort_keys=True)) print(report) if qemu != native: print("PARITY: FAIL") return 1 print("PARITY: OK (hook/watch/bp profiles identical)") return 0 if __name__ == "__main__": raise SystemExit(main())