diff --git a/chipflow_lib/common/sim/models.cc b/chipflow_lib/common/sim/models.cc index 213aaeb6..0aabda4f 100644 --- a/chipflow_lib/common/sim/models.cc +++ b/chipflow_lib/common/sim/models.cc @@ -75,7 +75,8 @@ void fetch_actions_into_queue() { void open_input_commands(const std::string &filename) { std::ifstream f(filename); if (!f) { - throw std::runtime_error("failed to open event log for writing!"); + std::cerr << "WARNING: failed to open input event stream. Add one for design validation - see [chipflow.test] configuration option." << std::endl; + return; } json data = json::parse(f); input_cmds = data["commands"]; diff --git a/chipflow_lib/config_models.py b/chipflow_lib/config_models.py index 6d523943..d3bf0a50 100644 --- a/chipflow_lib/config_models.py +++ b/chipflow_lib/config_models.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: BSD-2-Clause from enum import Enum +from pathlib import Path from typing import Dict, Optional, Any, List, Annotated from pydantic import ( @@ -62,6 +63,9 @@ class CompilerConfig(BaseModel): class SoftwareConfig(BaseModel): riscv: CompilerConfig = CompilerConfig(cpu="baseline_rv32-a-c-d", abi="ilp32") +class TestConfig(BaseModel): + event_reference: Path + class ChipFlowConfig(BaseModel): """Root configuration for chipflow.toml.""" project_name: str @@ -71,7 +75,7 @@ class ChipFlowConfig(BaseModel): simulation: SimulationConfig = SimulationConfig() software: SoftwareConfig = SoftwareConfig() clock_domains: Optional[List[str]] = None - + test: Optional[TestConfig] = None class Config(BaseModel): """Root configuration model for chipflow.toml.""" diff --git a/chipflow_lib/platforms/_utils.py b/chipflow_lib/platforms/_utils.py index 4346de5c..f2085b25 100644 --- a/chipflow_lib/platforms/_utils.py +++ b/chipflow_lib/platforms/_utils.py @@ -100,7 +100,7 @@ class IOModelOptions(TypedDict): init_oe: NotRequired[int | bool] -@pydantic.with_config(ConfigDict(arbitrary_types_allowed=True)) # type: ignore[reportCallIssue] +@pydantic.config.with_config(ConfigDict(arbitrary_types_allowed=True)) # type: ignore[reportCallIssue] class IOModel(IOModelOptions): """ Setting for IO Ports (see also base class `IOModelOptions`) diff --git a/chipflow_lib/steps/_json_compare.py b/chipflow_lib/steps/_json_compare.py new file mode 100644 index 00000000..ee2dc58e --- /dev/null +++ b/chipflow_lib/steps/_json_compare.py @@ -0,0 +1,20 @@ +import json +from pathlib import Path + +from .. import ChipFlowError + +def compare_events(gold_path: Path, gate_path: Path): + with open(gold_path, "r") as f: + gold = json.load(f) + with open(gate_path, "r") as f: + gate = json.load(f) + if len(gold["events"]) != len(gate["events"]): + raise ChipFlowError(f"Simulator check failed! Event mismatch: {len(gold['events'])} events in reference, {len(gate['events'])} in test output") + for ev_gold, ev_gate in zip(gold["events"], gate["events"]): + if ev_gold["peripheral"] != ev_gate["peripheral"] or \ + ev_gold["event"] != ev_gate["event"] or \ + ev_gold["payload"] != ev_gate["payload"]: + raise ChipFlowError(f"Simulator check failed! Reference event {ev_gold} mismatches test event {ev_gate}") + + return True + diff --git a/chipflow_lib/steps/sim.py b/chipflow_lib/steps/sim.py index 7d5207cc..cfa08a72 100644 --- a/chipflow_lib/steps/sim.py +++ b/chipflow_lib/steps/sim.py @@ -1,9 +1,10 @@ -import os +import inspect import importlib.resources import logging +import os +import subprocess from contextlib import contextmanager -from pathlib import Path from doit.cmd_base import TaskLoader2, loader from doit.doit_cmd import DoitMain @@ -12,8 +13,10 @@ from amaranth import Module from . import StepBase, _wire_up_ports +from ._json_compare import compare_events from .. import ChipFlowError, _ensure_chipflow_root -from ..platforms._utils import top_components +from ..cli import run +from ..platforms._utils import top_components, load_pinlock from ..platforms.sim import VARIABLES, TASKS, DOIT_CONFIG, SimPlatform @@ -28,14 +31,6 @@ def common(): with importlib.resources.as_file(common) as f: yield f -@contextmanager -def source(): - root = _ensure_chipflow_root() - sourcedir = Path(root) / 'design' / 'sim' - #sim_src = sourcedir.joinpath('design','sim') - #with importlib.resources.as_file(sim_src) as f: - yield sourcedir - @contextmanager def runtime(): yowasp = importlib.resources.files("yowasp_yosys") @@ -75,7 +70,34 @@ def __init__(self, config): self._platform = SimPlatform(config) self._config = config + def build_cli_parser(self, parser): + action_argument = parser.add_subparsers(dest="action") + action_argument.add_parser( + "build", help=inspect.getdoc(self.build).splitlines()[0]) # type: ignore + action_argument.add_parser( + "run", help=inspect.getdoc(self.run).splitlines()[0]) # type: ignore + action_argument.add_parser( + "check", help=inspect.getdoc(self.check).splitlines()[0]) # type: ignore + + def run_cli(self, args): + load_pinlock() # check pinlock first so we error cleanly + + match (args.action): + case "build": + self.build(args) + case "run": + self.run(args) + case "check": + self.check(args) + + @property + def sim_dir(self): + return _ensure_chipflow_root() / 'build' / 'sim' + def build(self, *args): + """ + Builds the simulation model for the design + """ print("Building simulation...") m = Module() self._platform.instantiate_ports(m) @@ -94,10 +116,9 @@ def build(self, *args): #FIXME: common source for build dir self._platform.build(m, top) - with common() as common_dir, source() as source_dir, runtime() as runtime_dir: + with common() as common_dir, runtime() as runtime_dir: context = { "COMMON_DIR": common_dir, - "SOURCE_DIR": source_dir, "RUNTIME_DIR": runtime_dir, "PROJECT_ROOT": _ensure_chipflow_root(), "BUILD_DIR": _ensure_chipflow_root() / 'build', @@ -107,3 +128,28 @@ def build(self, *args): context[k] = v.format(**context) if DoitMain(ContextTaskLoader(DOIT_CONFIG, TASKS, context)).run(["build_sim"]) !=0: raise ChipFlowError("Failed building simulator") + + def run(self, *args): + """ + Run the simulation. Will ensure that the simulation and the software are both built. + """ + run(["software"]) + self.build(args) + result = subprocess.run([self.sim_dir / "sim_soc"], cwd=self.sim_dir) + + if result.returncode != 0: + raise ChipFlowError("Simulation failed") + + def check(self, *args): + """ + Run the simulation and check events against reference (tests/events_reference.json). Will ensure that the simulation and the software are both built. + """ + if not self._config.chipflow.test: + raise ChipFlowError("No [chipflow.test] section found in configuration") + if not self._config.chipflow.test.event_reference: + raise ChipFlowError("No event_reference configuration found in [chipflow.test]") + + self.run(args) + compare_events(self._config.chipflow.test.event_reference, self.sim_dir / "events.json") + print("Integration test passed sucessfully") +