"""Native (M1) backend tests: demo parity with QEMU and engine determinism. The native simulator is pure Python and always available; every test still skips cleanly when the demo ELF cannot be loaded. The differential class additionally requires qemu-system-riscv64 and compares the scripted hook/watch/leave profile of both backends event for event. """ from __future__ import annotations import os import shutil import tempfile import unittest import greeneye as ge from greeneye.errors import RspTimeout HERE = os.path.dirname(os.path.abspath(__file__)) TOOL_ROOT = os.path.join(HERE, "..") DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf") RPC_HANDLERS = ["rpc_ping", "rpc_echo", "rpc_fill", "rpc_sum", "rpc_state"] def native_available() -> bool: try: import greeneye.sim_backend # noqa: F401 except Exception: return False return True def qemu_available() -> bool: return bool(os.environ.get("GREENEYE_QEMU") or shutil.which("qemu-system-riscv64")) def demo_available() -> bool: return os.path.exists(DEMO_ELF) and native_available() def launch(backend: str, record=None, name: str = "sim"): return ge.launch(kernel=DEMO_ELF, backend=backend, record=record, name=name) @unittest.skipUnless(native_available(), "native simulator unavailable") class NativeIsaTests(unittest.TestCase): """Exercise RV64M/A/C paths the demo firmware does not reach.""" # Assembled with llvm-mc -triple=riscv64 -mattr=+m,+a,+c (encodings are # data, so this runs without a toolchain): # li a0,6; li a1,7; mul a2,a0,a1; div a5,a1,a0; rem a6,a1,a0; # srai a4,a1,1; subw a3,a0,a1; lui s0,0x12345; addi sp,sp,-16; # sd a0,8(sp); ld s1,8(sp); lh s2,8(sp); lbu s3,8(sp); # amoadd.d s4,a0,(sp); lr.d s5,(sp); sc.d s6,a0,(sp); # li t0,1; c.addi t0,3; c.mv a2,t0; c.andi a2,15; ebreak PROGRAM = bytes.fromhex( "".join( """ 19 45 9d 45 33 06 b5 02 b3 c7 a5 02 33 e8 a5 02 13 d7 15 40 bb 06 b5 40 37 54 34 12 41 11 2a e4 a2 64 03 19 81 00 83 49 81 00 2f 3a a1 00 af 3a 01 10 2f 3b a1 18 85 42 8d 02 16 86 3d 8a 02 90 """.split() ) ) BASE = 0x80000000 STACK = 0x80004000 def test_m_a_c_execution(self): from sim.machine import Machine machine = Machine() machine.load_segments([(self.BASE, self.PROGRAM)], self.BASE) machine.set_reg(2, self.STACK) # sp reason, pc, _, _ = machine.run(1000) self.assertEqual(reason, 3) # R_EBREAK: stops before executing it self.assertEqual(pc, self.BASE + len(self.PROGRAM) - 2) expected = { 5: 4, # t0: c.addi 8: 0x12345000, # s0: lui 9: 6, # s1: c.sdsp/c.ldsp round-trip 10: 6, # a0 11: 7, # a1 12: 4, # a2: mul then c.mv/c.andi 13: 0xFFFFFFFFFFFFFFFF, # a3: subw sign extension 14: 3, # a4: srai 15: 1, # a5: div 16: 1, # a6: rem 18: 6, # s2: lh 19: 6, # s3: lbu 20: 0, # s4: amoadd old value 21: 6, # s5: lr 22: 0, # s6: sc success 2: self.STACK - 16, # c.addi16sp } got = {index: machine.x[index] for index in expected} self.assertEqual(got, expected) self.assertEqual(machine.read_mem(self.STACK - 16, 8), (6).to_bytes(8, "little")) @unittest.skipUnless(demo_available(), "demo firmware or native simulator unavailable") class NativeSmokeTests(unittest.TestCase): def test_start_sequence_sets_stack_and_enters_main(self): session = launch("native", name="unittest-sim-start") try: session.step(4) # auipc sp / addi sp / auipc ra / jalr main stack_top = next(s.value for s in session.image.symbols if s.name == "_stack_top") self.assertEqual(session.pc, session.symbols.lookup("main")) self.assertEqual(session.read_regs()["sp"], stack_top) finally: session.close() def test_boot_hook_watch_snapshot_analyze(self): root = tempfile.mkdtemp(prefix="greeneye-sim-") session = launch("native", record=root, name="unittest-sim-smoke") hits = [] try: ctrl = session.symbols.lookup("gsp_ctrl") session.hook("rpc_sum", on_enter=lambda hit: hits.append(hit)) watch_hits = [] session.watch(ctrl + 16, size=8, kind="w", callback=lambda hit: watch_hits.append(hit)) session.run(duration=0.6) self.assertTrue(session.target.wait_for_uart("GREEN-EYE UPROC DEMO", timeout=1.0)) self.assertGreaterEqual(len(hits), 1, "rpc_sum hook never fired") self.assertGreaterEqual(len(watch_hits), 7, "rpc_count watchpoint never fired") self.assertEqual(watch_hits[0].addr, ctrl + 16) self.assertEqual(watch_hits[0].watch.kind, "w") after = session.snapshot(ctrl, 0x188, "ctrl") self.assertEqual(int.from_bytes(after[0:4], "little"), 0x47535031) self.assertGreaterEqual(int.from_bytes(after[16:24], "little"), 7) finally: directory = getattr(session.recorder, "dir", None) session.close() from greeneye.analyze import render_summary report = render_summary(directory) self.assertIn("rpc_sum", report) self.assertIn("watch", report) def test_hook_args_and_leave_value(self): session = launch("native", name="unittest-sim-hookargs") events = [] try: ctrl = session.symbols.lookup("gsp_ctrl") session.hook( "rpc_sum", on_enter=lambda hit: events.append(("enter", hit.arg(0), hit.arg(1))), on_leave=lambda hit: events.append(("leave", hit.retval)), ) session.run(duration=0.2) # Self-test: rpc_dispatch(4, ctl->buf, 16, 0) after fill(0x5a, 32). self.assertIn(("enter", ctrl + 0x48, 16), events) self.assertIn(("leave", 16 * 0x5A), events) finally: session.close() def test_call_and_run_until(self): session = launch("native", name="unittest-sim-call") try: before = session.pc self.assertEqual(session.call("rpc_ping", (0x1111,)), 0x1111 ^ 0xBEEF) self.assertEqual(session.pc, before) hit = session.run_until("rpc_sum", timeout=2.0) self.assertEqual(hit.kind, "breakpoint") self.assertEqual(hit.addr, session.symbols.lookup("rpc_sum")) finally: session.close() def test_run_until_timeout(self): session = launch("native", name="unittest-sim-timeout") try: with self.assertRaises(RspTimeout): session.run_until("rpc_fault", timeout=0.05) # self-test never calls it finally: session.close() @unittest.skipUnless(demo_available(), "demo firmware or native simulator unavailable") class NativeDeterminismTests(unittest.TestCase): def _scripted_profile(self): """Drive fixed instruction budgets: no host clock in the result.""" session = ge.launch(kernel=DEMO_ELF, backend="native", name="unittest-sim-determinism") events = [] stops = [] try: session.hook( "rpc_sum", on_enter=lambda hit: events.append(("enter", hit.arg(0), hit.arg(1))), on_leave=lambda hit: events.append(("leave", hit.retval)), ) ctrl = session.symbols.lookup("gsp_ctrl") session.watch(ctrl + 16, size=8, kind="w") for _ in range(20): try: stop = session.client.continue_(timeout=0.02) # exactly 20k insns except RspTimeout: stops.append(("budget", session.pc)) continue session._dispatch_stop(stop) stops.append(("stop", session.pc)) ctrl_image = session.read_regs() snapshot = session.client.read_mem(ctrl, 0x188) finally: session.close() return events, stops, ctrl_image["pc"], snapshot def test_two_runs_identical(self): first = self._scripted_profile() second = self._scripted_profile() self.assertEqual(first, second) @unittest.skipUnless(demo_available() and qemu_available(), "native simulator and QEMU required") class DifferentialTests(unittest.TestCase): """Run one scripted scenario on both backends and compare the profiles.""" def _profile(self, backend: str): session = launch(backend, name=f"unittest-diff-{backend}") events = [] watch_hits = [] 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=0.3) uart = session.target.read_uart() bp_counts = [handle.hits for handle in session._bps.values()] finally: session.close() return events, len(watch_hits), bp_counts, uart.splitlines()[0] if uart else "" def test_hook_watch_profile_parity(self): qemu = self._profile("qemu") native = self._profile("native") self.assertEqual(qemu, native) self.assertEqual(native[1], 8) # 7 self-test dispatches + ctrl init self.assertEqual(native[2], [1, 1, 1, 1, 3]) if __name__ == "__main__": unittest.main()