diff --git a/docs/session-api.md b/docs/session-api.md index cb94582..e0f3f5e 100644 --- a/docs/session-api.md +++ b/docs/session-api.md @@ -28,13 +28,20 @@ Sim(scenario={...}, machine="c6", uart="uart0", cwd=fixture_dir) ``` Scenario dicts use the `sim --scenario` schema verbatim: `machines` -(`repl`/`mcu` + `overlay` + `elf`), `media` (BLE/CAN/Ethernet/UART buses), -`networkServices` (scripted peers), `quantum`. Relative paths resolve against -`cwd=` (default: the process cwd). - -`trace_symbols=[...]`, `trace_interrupts=True` and `show_logs=True` turn on the -non-halting instrumentation; read it back with `symbol_trace()`, `interrupts()` -and `logs()`. +(`repl`/`mcu` + `overlay` + `elf` + `symbolsElfPath`), `media` (BLE/CAN/Ethernet/UART +buses), `networkServices` (scripted peers), `quantum`. Relative paths resolve +against `cwd=` (default: the process cwd). + +`symbols_elf=` (single-machine form) / `symbolsElfPath` (scenario machine +entries — same key as the CLI's scenario YAML, so a scenario dict is +copy-pasteable) attaches a companion ELF that carries debug symbols for a +stripped image, e.g. a PlatformIO/IDF `firmware.elf` alongside a stripped +flash container passed as `elf=`. It only changes what `symbol()` and RTOS +introspection can resolve — the image that actually runs is still `elf=`. + +`trace_symbols=[...]`, `trace_memory=[...]`, `trace_interrupts=True` and +`show_logs=True` turn on the non-halting instrumentation; read it back with +`symbol_trace()`, `interrupts()` and `logs()`. ## Drive diff --git a/src/simantic/_rust.py b/src/simantic/_rust.py index 0282702..8d20522 100644 --- a/src/simantic/_rust.py +++ b/src/simantic/_rust.py @@ -25,14 +25,16 @@ class NotSupported(SimError): class RustBackend: def __init__(self, machines: list[dict], *, base: Path, media, services, quantum, - trace_symbols, trace_interrupts, engine_dir): + trace_symbols, trace_memory, trace_interrupts, engine_dir): if len(machines) != 1: raise NotSupported("backend='rust' runs one machine; multi-machine scenarios need backend='renode'") if media or services: raise NotSupported("backend='rust' has no media or network services yet (simantic-core#183)") - if trace_symbols or trace_interrupts: - raise NotSupported("backend='rust' has no symbol/interrupt tracing yet (simantic-core#183)") + if trace_symbols or trace_memory or trace_interrupts: + raise NotSupported("backend='rust' has no symbol/memory/interrupt tracing yet (simantic-core#183)") m = machines[0] + if m.get("symbolsElfPath"): + raise NotSupported("backend='rust' has no symbols_elf support yet (simantic-core#183)") self.machines = [m["name"]] self._elf = (base / m["elf"]).read_bytes() repl = base / m["repl"] if m.get("repl") else None diff --git a/src/simantic/session.py b/src/simantic/session.py index 1df1d5d..ec7098a 100644 --- a/src/simantic/session.py +++ b/src/simantic/session.py @@ -140,7 +140,9 @@ def __init__( scenario: dict[str, Any] | None = None, machine: str | None = None, uart: str = "uart0", + symbols_elf: str | os.PathLike[str] | None = None, trace_symbols: list[str] = (), + trace_memory: list[str] = (), trace_interrupts: bool = False, show_logs: bool = False, cwd: str | os.PathLike[str] | None = None, @@ -176,7 +178,8 @@ def __init__( if "elf" not in m or ("repl" in m) == ("mcu" in m): raise ValueError(f"machine {m['name']!r} needs elf and exactly one of repl/mcu") else: - machines = [{"name": "machine", "elf": elf, "repl": repl, "mcu": mcu, "overlay": overlay}] + machines = [{"name": "machine", "elf": elf, "repl": repl, "mcu": mcu, "overlay": overlay, + "symbolsElfPath": symbols_elf}] scenario = scenario or {} telemetry.record(f"sdk.session.{backend}") @@ -186,13 +189,14 @@ def __init__( self._b = RustBackend( machines, base=self._base, media=scenario.get("media"), services=scenario.get("networkServices"), quantum=scenario.get("quantum"), - trace_symbols=trace_symbols, trace_interrupts=trace_interrupts, engine_dir=engine_dir, + trace_symbols=trace_symbols, trace_memory=trace_memory, trace_interrupts=trace_interrupts, + engine_dir=engine_dir, ) else: self._b = _RenodeBackend( machines, base=self._base, work=self._work, media=scenario.get("media"), services=scenario.get("networkServices"), quantum=scenario.get("quantum"), - trace_symbols=trace_symbols, trace_interrupts=trace_interrupts, + trace_symbols=trace_symbols, trace_memory=trace_memory, trace_interrupts=trace_interrupts, show_logs=show_logs, engine_dir=engine_dir, ) self.machines: list[str] = list(self._b.machines) @@ -377,7 +381,7 @@ class _RenodeBackend: "interrupts": ("ReadInterrupts", _interrupt), "symbol_trace": ("ReadSymbolTrace", _symbol_trace)} def __init__(self, machines: list[dict], *, base: Path, work: Path, media, services, quantum, - trace_symbols, trace_interrupts, show_logs, engine_dir): + trace_symbols, trace_memory, trace_interrupts, show_logs, engine_dir): self._base, self._work = base, work ns = load(engine_dir) spec = ns.SessionSpec() @@ -385,8 +389,11 @@ def __init__(self, machines: list[dict], *, base: Path, work: Path, media, servi spec.ShowBackendLogs = show_logs for sym in trace_symbols: spec.TraceSymbols.Add(sym) + for region in trace_memory: + spec.TraceMemory.Add(region) for m in machines: - self._add_machine(spec, m["name"], m.get("repl"), m.get("mcu"), m.get("overlay"), m["elf"]) + self._add_machine(spec, m["name"], m.get("repl"), m.get("mcu"), m.get("overlay"), m["elf"], + m.get("symbolsElfPath")) for med in media or []: sm = spec.AddMedium(med["type"], list(med.get("connect") or [])) sm.Strict = bool(med.get("strict", False)) @@ -407,7 +414,7 @@ def __init__(self, machines: list[dict], *, base: Path, work: Path, media, servi raise SimError(f"could not start the simulation: {exc}") from exc self.machines = list(self._session.Machines) - def _add_machine(self, spec, name: str, repl, mcu, overlay, elf) -> None: + def _add_machine(self, spec, name: str, repl, mcu, overlay, elf, symbols_elf) -> None: """Platform file → AddMachine; model name → the local model library when $SIMANTIC_MCU_LIB is set (development), else the engine's own resolver (~/.sim_cache, then the backend with stored credentials — like `sim --mcu`).""" @@ -415,14 +422,15 @@ def _add_machine(self, spec, name: str, repl, mcu, overlay, elf) -> None: if repl is not None: if overlay is not None: raise ValueError("overlay= applies to mcu=, not repl=") - spec.AddMachine(name, str(self._base / repl), elf_path) - return - if os.environ.get(MCU_LIB_ENV): + sm = spec.AddMachine(name, str(self._base / repl), elf_path) + elif os.environ.get(MCU_LIB_ENV): platform = platform_path(mcu, self._base / overlay if overlay else None, self._work) - spec.AddMachine(name, str(platform), elf_path) - return - fragment = (self._base / overlay).read_text() if overlay else None - spec.AddModel(name, mcu, elf_path, fragment) + sm = spec.AddMachine(name, str(platform), elf_path) + else: + fragment = (self._base / overlay).read_text() if overlay else None + sm = spec.AddModel(name, mcu, elf_path, fragment) + if symbols_elf: + sm.SymbolsElfPath = str(self._base / symbols_elf) def _service_args(self, args: str) -> str: # A script path is the common case; make it absolute against cwd=. diff --git a/tests/test_session.py b/tests/test_session.py index 40da741..9cf9bf3 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -54,3 +54,27 @@ def test_expect_timeout_is_assertion(): with pytest.raises(ExpectTimeout) as exc: sim.expect("never printed by anything", timeout=2) assert isinstance(exc.value, AssertionError) + + +@needs_engine +def test_symbols_elf_single_machine(): + """symbols_elf= reaches SessionMachine.SymbolsElfPath (simantic-core#307) + without upsetting the run. No stripped-image fixture is wired up here, so + the companion ELF is the image itself — a smoke test of the plumbing + (loading the same ELF's symbols twice isn't a real symbol-resolution + check), not a claim that a stripped image + a genuinely separate + companion ELF resolves symbols (verified manually against + sim-fixtures/build/zephyr/zephyr.elf stripped with arm-none-eabi-strip).""" + with Sim(elf=ELF, repl=REPL, uart=UART, symbols_elf=ELF) as sim: + m = sim.expect(r"RESULT: (PASS|FAIL)", timeout=120) + assert "PASS" in m + + +@needs_engine +def test_symbols_elf_path_in_scenario(): + """symbolsElfPath in a scenario machine dict — same key as the CLI's + scenario YAML (simantic-cli#184), so a scenario dict stays copy-pasteable.""" + scenario = {"machines": {"machine": {"repl": REPL, "elf": ELF, "symbolsElfPath": ELF}}} + with Sim(scenario=scenario, uart=UART) as sim: + m = sim.expect(r"RESULT: (PASS|FAIL)", timeout=120) + assert "PASS" in m