From 002e3d7d13c414ea874abbee8d82803dd3a72fe5 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Rasmusson Date: Fri, 2 Oct 2026 10:58:21 -0400 Subject: debug: green-eye wave 3 - RM harness, local simulator, device map, MCP plugin, value-validation demos, CI pipeline, toolchain docs; add host RM tools rmtool/rmtop --- .../harness/value_demos/_common.py | 196 +++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 debug/risc-v-gsp-rm-vm/harness/value_demos/_common.py (limited to 'debug/risc-v-gsp-rm-vm/harness/value_demos/_common.py') diff --git a/debug/risc-v-gsp-rm-vm/harness/value_demos/_common.py b/debug/risc-v-gsp-rm-vm/harness/value_demos/_common.py new file mode 100644 index 0000000..f70ef1f --- /dev/null +++ b/debug/risc-v-gsp-rm-vm/harness/value_demos/_common.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Shared plumbing for the green-eye value-validation demos. + +Every demo maps one *real* Open-IOV scenario (cited from the handoffs) onto a +runnable demonstration on the bundled demo firmware / native RV64 simulator, +prints the question, the green-eye commands, the answer, the evidence artifact +and the measured wall-clock, and emits one machine-readable ``RESULT_JSON`` +line so ``run_all.py`` can aggregate. + +Stdlib only; no hardware; runs from the tool root +(``open-iov/debug/risc-v-gsp-rm-vm``). +""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import sys +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +TOOL_ROOT = os.path.dirname(os.path.dirname(HERE)) +if TOOL_ROOT not in sys.path: + sys.path.insert(0, TOOL_ROOT) + +ARTIFACT_ROOT = os.path.join(HERE, "artifacts") +DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") +SCHEMA_JSON = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "gsp_ctrl.schema.json") +SDK_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf") + +CTRL_MAGIC = 0x47535031 +CTRL_RPC_COUNT = 0x10 +CTRL_LAST_RPC = 0x18 +CTRL_MAILBOX = 0x20 +CTRL_ARG0 = 0x28 +CTRL_ARG1 = 0x30 +CTRL_ARG2 = 0x38 +CTRL_RESULT = 0x40 +CTRL_BUF = 0x48 +CTRL_SIZE = 0x190 +PING_XOR = 0xBEEF +INJECT_CMD_BASE = 16 +SERVICE_SLOT = 6 + +#: Per-demo wall-clock ceiling the runner enforces (subprocess timeout). +DEMO_TIMEOUT_S = 45.0 +#: Runner ceiling so the whole suite stays well under ~3 min. +SUITE_BUDGET_S = 180.0 + + +class DemoError(RuntimeError): + """A demo precondition failed in a way the caller should report, not mask.""" + + +def qemu_available() -> bool: + return bool(shutil.which("qemu-system-riscv64")) + + +def demo_available() -> bool: + return os.path.isfile(DEMO_ELF) + + +def choose_backend(preferred: str = "native") -> str: + """Environment override wins; QEMU is used only when preferred and present.""" + env = os.environ.get("VALUE_DEMO_BACKEND") + if env: + return env + if preferred == "qemu": + return "qemu" if qemu_available() else "native" + return "native" + + +def sha256_file(path: str) -> str: + digest = hashlib.sha256() + with open(path, "rb") as fh: + for chunk in iter(lambda: fh.read(1 << 20), b""): + digest.update(chunk) + return digest.hexdigest() + + +class Stopwatch: + def __init__(self) -> None: + self.t0 = time.monotonic() + self.elapsed = 0.0 + + def stop(self) -> float: + self.elapsed = time.monotonic() - self.t0 + return self.elapsed + + +def artifact_dir(name: str) -> str: + path = os.path.join(ARTIFACT_ROOT, name) + os.makedirs(path, exist_ok=True) + return path + + +def write_json(path: str, payload) -> str: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2, sort_keys=True) + fh.write("\n") + return path + + +def write_text(path: str, text: str) -> str: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as fh: + fh.write(text) + return path + + +def load_schema(ge): + with open(SCHEMA_JSON, "r", encoding="utf-8") as fh: + return ge.schema_from_json(json.load(fh)) + + +def decode_ctrl(ge, data: bytes, schema=None) -> dict: + schema = schema or load_schema(ge) + out = schema.decode(data, base=0) + if isinstance(out.get("buf"), (bytes, bytearray)): + out["buf"] = bytes(out["buf"]).split(b"\x00", 1)[0].decode("utf-8", "replace") + return out + + +def short_hex(value) -> str: + if value is None: + return "None" + if isinstance(value, str): + return value + return f"{value:#x}" if isinstance(value, int) else str(value) + + +def render_banner(demo: str, question: str, scenario: str, baseline: str, commands) -> None: + print(f"=== {demo} ===") + print(f"real scenario : {scenario}") + print(f"baseline : {baseline}") + print(f"question : {question}") + print("commands :") + for command in commands: + print(f" - {command}") + + +def finish(result: dict, artifact: str, elapsed: float) -> dict: + """Persist the per-demo result and print the human + machine summary.""" + result["elapsed_s"] = round(elapsed, 4) + result["artifact"] = os.path.relpath(artifact, TOOL_ROOT) if artifact else None + result.setdefault("success", True) + if artifact: + write_json(os.path.join(artifact, "result.json"), result) + print(f"answer : {result.get('answer')}") + print(f"evidence : {result.get('artifact')}") + print(f"analog time : {elapsed:.3f} s ({result.get('backend')} backend)") + if result.get("real_baseline"): + print(f"real baseline : {result['real_baseline']}") + if result.get("projected"): + print(f"projected : {result['projected']}") + if result.get("blocked_on_device_model"): + print("blocked : real case blocked on H2 device model; workflow shown on analog") + if not result.get("success", True): + print(f"status : FAILED - {result.get('failure', 'unknown')}") + print("RESULT_JSON: " + json.dumps(result, sort_keys=True)) + sys.stdout.flush() + return result + + +def timed_demo(fn, artifact: str) -> dict: + watch = Stopwatch() + try: + result = fn() + except Exception as exc: # report, never hide + import traceback + + result = { + "success": False, + "failure": f"{type(exc).__name__}: {exc}", + "traceback": traceback.format_exc()[-2000:], + } + return finish(result, artifact, watch.stop()) + + +def scenario( + *, + ids, + baseline, + projection, + blocked=False, + device=None, +) -> dict: + return { + "citation_ids": ids, + "static_baseline": baseline, + "projection": projection, + "blocked_on_device_model": blocked, + "device_model_needed": device, + } -- cgit v1.2.3