from __future__ import annotations import hashlib import importlib.util import io import json import re import signal import subprocess import sys import tarfile import time from pathlib import Path from types import SimpleNamespace from zipfile import ZipFile import pytest ROOT = Path(__file__).resolve().parents[1] FILELIST_SCRIPT_DIR = ROOT / "rtl/mini/script" FORMAL_SCRIPT_DIR = ROOT / "rtl/mini/formal" sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(FILELIST_SCRIPT_DIR)) sys.path.insert(0, str(FORMAL_SCRIPT_DIR)) from filelist import atomic_write, parse_filelists, write_filelist # noqa: E402 from generate_filelist import generate_all # noqa: E402 from generate_formal_filelist import generate as generate_formal_filelist # noqa: E402 from generate_sby_config import render as render_sby_config # noqa: E402 from scripts.bitwuzla_smt2 import translate_arguments # noqa: E402 from scripts.analyze_warnings import normalize # noqa: E402 from scripts.check_c_warnings import self_owned_warnings # noqa: E402 from scripts.check_format import format_files # noqa: E402 from scripts.dependency_lock import LockError, load_lock, validate_flake_lock # noqa: E402 from scripts.development_environment import ( # noqa: E402 DEFAULT_TOOLS, render_activation, stamp_data, write_activation, write_stamp, ) from scripts.generate_mpw import render_active_manifest, validate_extension_bindings # noqa: E402 from scripts.install_toolchain import make_tree_world_readable, safe_extract # noqa: E402 from scripts.package import make_sbom # noqa: E402 from scripts.prepare_mpw import patch_serv # noqa: E402 from scripts import regress # noqa: E402 from scripts import run_flow # noqa: E402 from scripts.regress import ( # noqa: E402 CI_SMOKE_APP_VALUE, NIGHTLY_COMMANDS, NIGHTLY_EXTRA_COMMANDS, PDK_PR_PROFILES, PR_COMMANDS, RTL_COMMANDS, RTL_LINT_VALUES, SMOKE_COMMANDS, behavioral_only, pdk_pr_commands, regression_environment, select_regression, ) from scripts import setup_helpers # noqa: E402 from scripts import setup_apu_reference # noqa: E402 from scripts.setup_helpers import download_file, ensure_git_repo # noqa: E402 def run(*command: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: return subprocess.run( command, cwd=cwd or ROOT, text=True, capture_output=True, check=True, ) def test_atomic_write_preserves_unchanged_mtime(tmp_path: Path) -> None: output = tmp_path / "output.fl" assert atomic_write(output, "same\n") is True first_mtime = output.stat().st_mtime_ns assert atomic_write(output, "same\n") is False assert output.stat().st_mtime_ns == first_mtime def test_apu_reference_reuses_verified_extracted_source( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: destination = tmp_path / "sources/apu-reference" destination.mkdir(parents=True) (destination / ".retrosoc-archive-sha256").write_text("locked-sha\n", encoding="utf-8") monkeypatch.setattr(setup_apu_reference, "ROOT", tmp_path) monkeypatch.setattr( setup_apu_reference, "source", lambda _name: {"destination": "sources/apu-reference"}, ) monkeypatch.setattr( setup_apu_reference, "archive", lambda _name: { "destination": "downloads/apu-reference.tar.gz", "url": "https://example.invalid/apu-reference.tar.gz", "sha256": "locked-sha", }, ) monkeypatch.setattr( setup_apu_reference, "download_file", lambda *_args, **_kwargs: pytest.fail("verified source must not be downloaded again"), ) assert setup_apu_reference._install_archive("apu-reference", update=False, timeout=1) == destination def test_nested_filelist_and_space_path_round_trip(tmp_path: Path) -> None: include_dir = tmp_path / "include files" include_dir.mkdir() source = tmp_path / "source file.sv" library = tmp_path / "library file.v" source.write_text("module source_file; endmodule\n", encoding="utf-8") library.write_text("module library_file; endmodule\n", encoding="utf-8") nested = tmp_path / "nested.fl" nested.write_text( f'"{source}"\n-v "{library}"\n', encoding="utf-8", ) top = tmp_path / "top.fl" top.write_text( f'+define+TEST=1\n"+incdir+{include_dir}"\n-f "{nested}"\n', encoding="utf-8", ) parsed = parse_filelists([top]) assert parsed.defines == ["+define+TEST=1"] assert parsed.incdirs == [include_dir.resolve()] assert parsed.files == [source.resolve()] assert parsed.library_files == [library.resolve()] output = tmp_path / "round trip.fl" write_filelist(output, parsed) assert parse_filelists([output]) == parsed def test_generate_all_is_stable_and_expands_paths(tmp_path: Path) -> None: defines = ["+define+PDK_IHP130"] generated_include = tmp_path / "archinfo metadata" generated = generate_all(tmp_path, defines, [generated_include]) mtimes = {path: path.stat().st_mtime_ns for path in generated} generate_all(tmp_path, defines, [generated_include]) assert {path: path.stat().st_mtime_ns for path in generated} == mtimes assert (tmp_path / "def.fl").read_text(encoding="utf-8") == ( f"+incdir+{generated_include.resolve()} " + " ".join(defines) + "\n" ) cluster = (tmp_path / "clusterip.fl").read_text(encoding="utf-8") hazard3 = (tmp_path / "core_hazard3.fl").read_text(encoding="utf-8") ihp130 = (tmp_path / "pdk_ihp130.fl").read_text(encoding="utf-8") assert str(ROOT / "rtl/managed/clusterip") in cluster assert str(ROOT / "rtl/managed/hazard3/hdl") in hazard3 assert str(ROOT / "physical/pdk/IHP-Open-PDK") in ihp130 assert "rtl/managed/mpw/core/username3" not in hazard3 assert (tmp_path / "core_hazard3.fl").is_file() assert not (tmp_path / "core_picorv32.fl").exists() assert { "pdk_gf180.fl", "pdk_ics55.fl", "pdk_ics55_verilator.fl", "pdk_ics55_yosys.fl", "pdk_ihp130.fl", "pdk_sky130.fl", }.issubset({path.name for path in generated}) def test_netlist_support_filelist_is_an_explicit_testbench_allowlist(tmp_path: Path) -> None: generate_all(tmp_path, ["+define+PDK_IHP130"]) support = parse_filelists([tmp_path / "netlist_support.fl"]) source_names = {path.name for path in support.files} assert source_names == {"register.sv", "rst_sync.sv"} def test_filelist_round_trips_verilog_define_values(tmp_path: Path) -> None: generate_all(tmp_path, ["+define+SOC_JTAG_IDCODE=32'hDEADBEEF"]) assert (tmp_path / "def.fl").read_text(encoding="utf-8") == ( "+define+SOC_JTAG_IDCODE=32'hDEADBEEF\n" ) parsed = parse_filelists([tmp_path / "def.fl"]) output = tmp_path / "round-trip.fl" write_filelist(output, parsed) assert output.read_text(encoding="utf-8") == ("+define+SOC_JTAG_IDCODE=32'hDEADBEEF\n") assert parse_filelists([output]) == parsed def test_source_export_uses_the_fixed_hazard3_management_core(tmp_path: Path) -> None: module_path = ROOT / "physical/smoke/syn/tools/export_soc_sources.py" spec = importlib.util.spec_from_file_location("retrosoc_source_export", module_path) assert spec is not None and spec.loader is not None source_export = importlib.util.module_from_spec(spec) spec.loader.exec_module(source_export) dynamic_core = tmp_path / "core.fl" dynamic_ip = tmp_path / "ip.fl" dynamic_core.write_text("", encoding="utf-8") dynamic_ip.write_text("", encoding="utf-8") arguments = SimpleNamespace( pdk="IHP130", simu="IVERILOG", have_pll=False, have_sram_if=False, have_sram_macro=False, have_sva=False, jtag_idcode="DEADBEEF", dynamic_core_filelist=dynamic_core, dynamic_ip_filelist=dynamic_ip, ) generated_dir = tmp_path / "hazard3" user_extensions_dir = generated_dir / "user_extensions" source_export.generate_all(generated_dir, source_export.build_defines(arguments)) source_export.generate_user_extensions( ROOT / "rtl/mini/integration/user_extensions.json", user_extensions_dir ) filelist = source_export.configured_filelist( arguments, generated_dir, user_extensions_dir, require_files=False ) assert "+define+SOC_JTAG_IDCODE=-559038737" in filelist.defines assert "+define+SOC_EXT_CLK_HZ=72000000" in filelist.defines assert "+define+SOC_AUD_CLK_HZ=18432000" in filelist.defines assert "+define+SOC_CLINT_TIMEBASE_HZ=1000000" in filelist.defines assert not any(item.startswith("+define+CORE_") for item in filelist.defines) assert "+define+HAVE_DEBUG" not in filelist.defines assert any(path.name == "hazard3_cpu_1port.v" for path in filelist.files) def test_librelane_source_export_inlines_hazard3_update_helper() -> None: module_path = ROOT / "physical/smoke/syn/tools/export_soc_sources.py" spec = importlib.util.spec_from_file_location("retrosoc_librelane_export", module_path) assert spec is not None and spec.loader is not None source_export = importlib.util.module_from_spec(spec) spec.loader.exec_module(source_export) source = """function [XLEN-1:0] update_nonconst; input [XLEN-1:0] prev; input [XLEN-1:0] nonconst; begin update_nonconst = (wdata_update & nonconst) | (prev & ~nonconst); end endfunction mtvec_reg <= update_nonconst(mtvec_reg, MTVEC_WMASK); mie <= update_nonconst(mie, MIE_WMASK); """ sanitized = source_export.sanitize_librelane_source(source) assert "update_nonconst" not in sanitized assert "mtvec_reg <= ((wdata_update & MTVEC_WMASK) | (mtvec_reg & ~MTVEC_WMASK));" in sanitized assert "mie <= ((wdata_update & MIE_WMASK) | (mie & ~MIE_WMASK));" in sanitized def test_package_forwards_manifest_jtag_idcode() -> None: package_source = (ROOT / "scripts/package.py").read_text(encoding="utf-8") assert '"--jtag-idcode"' in package_source assert 'config.get("JTAG_IDCODE", "DEADBEEF")' in package_source assert '"--core"' not in package_source for option in ( "--ext-clk-hz", "--aud-clk-hz", "--clint-timebase-hz", "--memory-map-filelist", "--soc-topology-filelist", "--user-extensions-filelist", "--pin-map-filelist", "--archinfo-incdir", "--metadata-file", ): assert f'"{option}"' in package_source assert "generate_timing_contract.py" in package_source assert "commercial_timing_contract.tcl" in package_source def test_source_export_writes_tar_without_staging_rtl_tree(tmp_path: Path) -> None: module_path = ROOT / "physical/smoke/syn/tools/export_soc_sources.py" spec = importlib.util.spec_from_file_location("retrosoc_tar_export", module_path) assert spec is not None and spec.loader is not None source_export = importlib.util.module_from_spec(spec) spec.loader.exec_module(source_export) source = ROOT / "rtl/mini/top/retrosoc_asic.sv" filelist = parse_filelists([], require_files=False) filelist.files.append(source) contract = tmp_path / "commercial_timing_contract.tcl" contract.write_text("set flow::contract 1\n", encoding="utf-8") archive = source_export.write_tar( filelist, tmp_path, "MINI", {Path("contracts/commercial_timing_contract.tcl"): contract}, ) assert not (tmp_path / "rtl").exists() with tarfile.open(archive) as bundle: names = bundle.getnames() assert "rtl/mini/top/retrosoc_asic.sv" in names assert bundle.extractfile("rtl/filelist.fl").read().decode() == ( "mini/top/retrosoc_asic.sv\n" ) assert "rtl/contracts/commercial_timing_contract.tcl" in names assert "contracts/commercial_timing_contract.tcl" not in ( bundle.extractfile("rtl/filelist.fl").read().decode() ) def test_source_export_bundles_generated_include(tmp_path: Path) -> None: module_path = ROOT / "physical/smoke/syn/tools/export_soc_sources.py" spec = importlib.util.spec_from_file_location("retrosoc_include_export", module_path) assert spec is not None and spec.loader is not None source_export = importlib.util.module_from_spec(spec) spec.loader.exec_module(source_export) include_dir = tmp_path / "generated/archinfo" include_dir.mkdir(parents=True) header = include_dir / "archinfo_integration_metadata.svh" header.write_text("`define ARCHINFO_TEST 1\n", encoding="utf-8") source = tmp_path / "design.sv" source.write_text( '`include "archinfo_integration_metadata.svh"\nmodule design; endmodule\n', encoding="utf-8", ) filelist = parse_filelists([], require_files=False) filelist.incdirs.append(include_dir) filelist.files.append(source) archive = source_export.write_tar(filelist, tmp_path / "output", "MINI") with tarfile.open(archive) as bundle: names = bundle.getnames() assert f"rtl/{source_export.bundle_relative(header)}" in names filelist_text = bundle.extractfile("rtl/filelist.fl").read().decode() assert f"+incdir+{source_export.bundle_relative(include_dir)}" in filelist_text def test_formal_filelists_are_scoped_to_the_protocol_duts(tmp_path: Path) -> None: memory_map = tmp_path / "memory_map" topology = tmp_path / "soc_topology" user_extensions = tmp_path / "user_extensions" (memory_map / "rtl").mkdir(parents=True) (topology / "rtl").mkdir(parents=True) (user_extensions / "rtl").mkdir(parents=True) bus_filelist = tmp_path / "bus.fl" rib_adapter_filelist = tmp_path / "rib_adapter.fl" rib2apb_filelist = tmp_path / "rib2apb.fl" sysctrl_filelist = tmp_path / "sysctrl.fl" pll_rcu_filelist = tmp_path / "pll_rcu.fl" gpio_filelist = tmp_path / "gpio.fl" ws2812_filelist = tmp_path / "ws2812.fl" i2c_filelist = tmp_path / "i2c.fl" clint_filelist = tmp_path / "clint.fl" onchip_ram_filelist = tmp_path / "onchip_ram.fl" opipsram_filelist = tmp_path / "opipsram.fl" ga2d_filelist = tmp_path / "ga2d.fl" apu_filelist = tmp_path / "apu.fl" apu_primitives_filelist = tmp_path / "apu_primitives.fl" apu_loader_filelist = tmp_path / "apu_loader.fl" apu_sequencer_filelist = tmp_path / "apu_sequencer.fl" gateway_a_filelist = tmp_path / "gateway_a.fl" npu_dma_filelist = tmp_path / "npu_dma.fl" assert generate_formal_filelist("bus", bus_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist( "rib_adapter", rib_adapter_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "rib2apb", rib2apb_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "sysctrl", sysctrl_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "pll_rcu", pll_rcu_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist("gpio", gpio_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist( "ws2812", ws2812_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist("i2c", i2c_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist("clint", clint_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist( "onchip_ram", onchip_ram_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "opipsram", opipsram_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist("ga2d", ga2d_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist("apu", apu_filelist, memory_map, topology, user_extensions) assert generate_formal_filelist( "apu_primitives", apu_primitives_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "apu_loader", apu_loader_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "apu_sequencer", apu_sequencer_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "gateway_a", gateway_a_filelist, memory_map, topology, user_extensions ) assert generate_formal_filelist( "npu_dma", npu_dma_filelist, memory_map, topology, user_extensions ) bus = parse_filelists([bus_filelist], require_files=False) rib_adapter = parse_filelists([rib_adapter_filelist], require_files=False) rib2apb = parse_filelists([rib2apb_filelist], require_files=False) sysctrl = parse_filelists([sysctrl_filelist], require_files=False) pll_rcu = parse_filelists([pll_rcu_filelist], require_files=False) gpio = parse_filelists([gpio_filelist], require_files=False) ws2812 = parse_filelists([ws2812_filelist], require_files=False) i2c = parse_filelists([i2c_filelist], require_files=False) clint = parse_filelists([clint_filelist], require_files=False) onchip_ram = parse_filelists([onchip_ram_filelist], require_files=False) opipsram = parse_filelists([opipsram_filelist], require_files=False) ga2d = parse_filelists([ga2d_filelist], require_files=False) apu = parse_filelists([apu_filelist], require_files=False) apu_primitives = parse_filelists([apu_primitives_filelist], require_files=False) apu_loader = parse_filelists([apu_loader_filelist], require_files=False) apu_sequencer = parse_filelists([apu_sequencer_filelist], require_files=False) gateway_a = parse_filelists([gateway_a_filelist], require_files=False) npu_dma = parse_filelists([npu_dma_filelist], require_files=False) assert "+define+SV_ASSRT_DISABLE" in bus.defines assert "+define+PDK_BEHAV" in onchip_ram.defines assert "+define+SYNTHESIS" in onchip_ram.defines assert "+define+PDK_BEHAV" in opipsram.defines assert "+define+SYNTHESIS" not in opipsram.defines assert "+define+SV_ASSRT_DISABLE" not in opipsram.defines assert ROOT / "rtl/mini/top/rib_bus.sv" in bus.files assert ROOT / "rtl/mini/top/rib_if.sv" in bus.files assert ROOT / "rtl/mini/top/ribp2rib.sv" in bus.files assert ROOT / "rtl/mini/top/rib_error_slave.sv" in bus.files assert ROOT / "rtl/mini/formal/bus_formal.sv" in bus.files assert ROOT / "rtl/mini/top/rib2ribp.sv" in rib_adapter.files assert ROOT / "rtl/mini/formal/rib_adapter_formal.sv" in rib_adapter.files assert ROOT / "rtl/mini/top/rib2apb.sv" in rib2apb.files assert ROOT / "rtl/mini/formal/rib2apb_formal.sv" in rib2apb.files assert ROOT / "rtl/managed/clusterip/common/rtl/interface/apb4_pure_if.sv" in rib2apb.files assert ROOT / "rtl/ip/peripheral/sysctrl_if.sv" in sysctrl.files assert ROOT / "rtl/ip/peripheral/sysctrl_define.svh" in sysctrl.files assert ROOT / "rtl/ip/peripheral/sysctrl_reg.sv" in sysctrl.files assert ROOT / "rtl/ip/peripheral/sysctrl_core.sv" in sysctrl.files assert ROOT / "rtl/ip/peripheral/apb4_sysctrl.sv" in sysctrl.files assert ROOT / "rtl/managed/clusterip/common/rtl/cdc/cdc_sync.sv" in sysctrl.files assert ROOT / "rtl/mini/formal/sysctrl_formal.sv" in sysctrl.files assert ROOT / "rtl/mini/top/rcu.sv" in pll_rcu.files assert ROOT / "rtl/mini/top/pll_rcu_controller.sv" in pll_rcu.files assert ROOT / "rtl/managed/clusterip/common/rtl/cdc/cdc_2phase.sv" in pll_rcu.files assert ROOT / "rtl/managed/clusterip/common/rtl/clkrst/rst_sync.sv" in pll_rcu.files assert ROOT / "rtl/managed/clusterip/common/rtl/interface/apb4_if.sv" in gpio.files assert ROOT / "rtl/ip/peripheral/gpio_core.sv" in gpio.files assert ROOT / "rtl/ip/peripheral/gpio_reg.sv" in gpio.files assert ROOT / "rtl/ip/peripheral/apb4_gpio.sv" in gpio.files assert ROOT / "rtl/ip/peripheral/user_gpio_if.sv" in gpio.files assert ROOT / "rtl/managed/clusterip/common/rtl/cdc/cdc_sync.sv" in gpio.files assert ROOT / "rtl/mini/formal/gpio_formal.sv" in gpio.files assert ROOT / "rtl/managed/clusterip/common/rtl/interface/apb4_if.sv" in ws2812.files assert ROOT / "rtl/ip/serial/apb4_ws2812.sv" in ws2812.files assert ROOT / "rtl/managed/clusterip/common/rtl/utils/fifo.sv" in ws2812.files assert ROOT / "rtl/mini/formal/ws2812_formal.sv" in ws2812.files assert ROOT / "rtl/ip/serial/apb4_i2c.sv" in i2c.files assert ROOT / "rtl/ip/serial/i2c_filter.sv" in i2c.files assert ROOT / "rtl/mini/formal/i2c_formal.sv" in i2c.files assert ROOT / "rtl/ip/peripheral/apb4_clint.sv" in clint.files assert ROOT / "rtl/mini/formal/clint_formal.sv" in clint.files assert ROOT / "rtl/ip/memory/opipsram_axi4.sv" in opipsram.files assert ROOT / "rtl/ip/memory/opipsram_core.sv" in opipsram.files assert ROOT / "rtl/ip/memory/opipsram_reg.sv" in opipsram.files assert ROOT / "rtl/ip/memory/opipsram_protocol.sv" in opipsram.files assert ROOT / "rtl/ip/memory/opipsram_phy.sv" in opipsram.files assert ROOT / "rtl/ip/memory/opipsram_trx.sv" in opipsram.files assert ROOT / "rtl/ip/util/async_fifo.sv" in opipsram.files assert ROOT / "rtl/managed/clusterip/common/rtl/utils/gray2bin.sv" in opipsram.files assert ROOT / "rtl/managed/clusterip/common/rtl/utils/xchecker.sv" in opipsram.files assert ROOT / "rtl/tech/tc_clk.sv" in opipsram.files assert ROOT / "rtl/tech/tc_opipsram_delay.sv" in opipsram.files assert ROOT / "rtl/mini/formal/opipsram_formal.sv" in opipsram.files assert ROOT / "rtl/ip/multimedia/ga2d_pkg.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_addr_gen.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_axi4_master.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_dma.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_pixel.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_core.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/ga2d_reg.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/apb4_ga2d.sv" in ga2d.files assert ROOT / "rtl/mini/formal/ga2d_formal.sv" in ga2d.files assert ROOT / "rtl/ip/multimedia/apu_dma.sv" in apu.files assert ROOT / "rtl/mini/formal/apu_formal.sv" in apu.files assert ROOT / "rtl/ip/multimedia/apu_resampler.sv" in apu_primitives.files assert ROOT / "rtl/ip/multimedia/apu_local_sram.sv" in apu_primitives.files assert ROOT / "rtl/ip/multimedia/apu_kernel_engine.sv" in apu_primitives.files assert ROOT / "rtl/ip/multimedia/apu_primitive_dispatcher.sv" in apu_primitives.files assert ROOT / "rtl/managed/clusterip/common/rtl/utils/fifo.sv" in apu_primitives.files assert ROOT / "rtl/mini/formal/apu_primitives_formal.sv" in apu_primitives.files formal_makefile = (ROOT / "rtl/mini/mk/formal.mk").read_text(encoding="utf-8") for scenario in ( "apu_primitives_invalid_read", "apu_primitives_local", "apu_primitives_kernel", "apu_primitives_input_fifo", "apu_primitives_alignment", "apu_primitives_output_fifo", ): assert scenario in formal_makefile assert ROOT / "rtl/ip/multimedia/apu_microcode_loader.sv" in apu_loader.files assert ROOT / "rtl/mini/formal/apu_loader_formal.sv" in apu_loader.files assert ROOT / "rtl/ip/multimedia/apu_codec_sequencer.sv" in apu_sequencer.files assert ROOT / "rtl/mini/formal/apu_sequencer_formal.sv" in apu_sequencer.files assert ROOT / "rtl/mini/top/hp_axi4_mux3.sv" in gateway_a.files assert ( ROOT / "rtl/managed/clusterip/common/rtl/stream/round_robin_arbiter.sv" in gateway_a.files ) assert ROOT / "rtl/mini/formal/gateway_a_formal.sv" in gateway_a.files assert "+define+SV_ASSRT_DISABLE" in npu_dma.defines assert ROOT / "rtl/ip/multimedia/npu_dma.sv" in npu_dma.files assert ROOT / "rtl/managed/clusterip/common/rtl/interface/axi4_if.sv" in npu_dma.files assert ROOT / "rtl/mini/formal/npu_dma_formal.sv" in npu_dma.files def test_opipsram_formal_keeps_full_depth_with_hosted_runner_budget() -> None: formal_makefile = (ROOT / "rtl/mini/mk/formal.mk").read_text(encoding="utf-8") assert re.search(r"^FORMAL_OPIPSRAM_DEPTH\s+\?= 40$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_OPIPSRAM_BMC_DEPTH\s+\?= 40$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_OPIPSRAM_TIMEOUT\s+\?= 300$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_OPIPSRAM_BMC_TIMEOUT\s+\?= 600$", formal_makefile, re.MULTILINE) assert "$(FORMAL_OPIPSRAM_BMC_TIMEOUT)s $(FORMAL_SBY)" in formal_makefile def test_ga2d_formal_target_uses_the_p5_production_hierarchy() -> None: formal_makefile = (ROOT / "rtl/mini/mk/formal.mk").read_text(encoding="utf-8") filelist_generator = ( ROOT / "rtl/mini/formal/generate_formal_filelist.py" ).read_text(encoding="utf-8") formal = (ROOT / "rtl/mini/formal/ga2d_formal.sv").read_text(encoding="utf-8") properties = (ROOT / "rtl/mini/formal/ga2d_formal_props.sv").read_text( encoding="utf-8" ) assert re.search(r"^FORMAL_GA2D_DEPTH\s+\?= 36$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_GA2D_COVER_DEPTH\s+\?= 199$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_GA2D_TIMEOUT\s+\?= 7200$", formal_makefile, re.MULTILINE) assert "formal-ga2d: $(FORMAL_DIR)/ga2d/.stamp | manifest" in formal_makefile assert 'if target == "ga2d":' in filelist_generator for source in ( "ga2d_pkg.sv", "ga2d_addr_gen.sv", "ga2d_axi4_master.sv", "ga2d_dma.sv", "ga2d_pixel.sv", "ga2d_core.sv", "ga2d_reg.sv", "apb4_ga2d.sv", "ga2d_formal.sv", ): assert source in filelist_generator assert "ga2d_formal_design u_design" in formal assert "ga2d_core u_dut" in properties assert "assert (awlen <= 8'd15);" in formal assert "assert (arlen <= 8'd15);" in formal assert "assert ((arlen == 8'd0) || (arsize == 3'd3));" in formal assert "assert (output_fifo_count >= ({1'b0, awlen} + 9'd1));" in formal assert "if (s_read_inflight_q && !bridge_clear_busy_i)" in formal assert "assert (wstrb != 8'd0);" in formal assert "cover (scenario == 4'd0 && done" in formal assert "cover (scenario == 4'd3 && aborted && safe_idle);" in formal assert "cover (scenario == 4'd2 && awvalid && awready && (awlen == 8'd14));" in formal assert "cover (scenario == 4'd7 && awvalid && awready && (awlen == 8'd15));" in formal assert "protocol_residual_rvalid" in formal assert "assert (!rready);" in formal assert "assert (!arvalid);" in formal assert "cover ((scenario == 4'd8) && protocol_residual_rvalid" in formal assert "assert (read_owner == $past(read_owner));" in formal assert "assert ({1'b0, bg_captured_pixels} >= write_cover_end_pixel);" in formal assert "cover (scenario == 4'd9 && done && safe_idle" in formal assert "cover (scenario == 4'd10 && done && safe_idle && inplace_background" in formal assert "output logic [3:0] scenario" in properties assert "s_residual_r_valid_q" in properties assert "OperationBlend" in properties assert "FormatA8" in properties def test_npu_dma_formal_target_pins_the_bounded_axi4_contract() -> None: formal_makefile = (ROOT / "rtl/mini/mk/formal.mk").read_text(encoding="utf-8") filelist_generator = ( ROOT / "rtl/mini/formal/generate_formal_filelist.py" ).read_text(encoding="utf-8") design = (ROOT / "rtl/mini/formal/npu_dma_formal.sv").read_text(encoding="utf-8") properties = (ROOT / "rtl/mini/formal/npu_dma_formal_props.sv").read_text( encoding="utf-8" ) assert re.search(r"^FORMAL_NPU_DMA_DEPTH\s+\?= 16$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_NPU_DMA_COVER_DEPTH\s+\?= 32$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_NPU_DMA_TIMEOUT\s+\?= 7200$", formal_makefile, re.MULTILINE) assert re.search(r"^FORMAL_TARGETS\s+:=.* npu_dma$", formal_makefile, re.MULTILINE) assert "formal-npu_dma: $(FORMAL_DIR)/npu_dma/.stamp | manifest" in formal_makefile assert 'if target == "npu_dma":' in filelist_generator for source in ("npu_dma.sv", "npu_dma_formal.sv"): assert source in filelist_generator assert "npu_dma u_dut" in design assert "(* anyconst *)logic [ 1:0] f_read_fault;" in design assert "(* anyconst *)logic [ 3:0] f_block_start;" in design assert "(* anyseq *)logic f_write_last;" in design assert "s_r_proto_rlast_q" in design assert "s_w_bid_bad_q" in design assert "npu_dma_formal_design u_design" in properties assert "assert (arlen <= 8'd15);" in properties assert "assert (awlen <= 8'd15);" in properties assert "assert (awsize == 3'd3);" in properties assert "assert ((arsize == 3'd3) || (arlen == 8'd0));" in properties assert "assert (araddr == f_rd_next_burst_q);" in properties assert "assert ({4'd0, wfifo_count} == ({1'b0, awlen} + 9'd1));" in properties assert "assert (!f_rd_outstanding_q);" in properties assert "assert (f_wr_aw_q);" in properties assert "assert (fault_code == 4'd6);" in properties assert "assert (fault_code == 4'd4);" in properties assert "assert (fault_code == 4'd5);" in properties assert "assume (read_bytes <= 32'd128);" in properties assert "cover (pause_ack && arvalid && !arready);" in properties assert "cover (clear && f_rd_outstanding_q);" in properties assert "cover (fault && (fault_code == 4'd6));" in properties assert "cover (write_done);" in properties def test_sysctrl_formal_properties_use_exported_user_core_shape() -> None: design = (ROOT / "rtl/mini/formal/sysctrl_formal.sv").read_text(encoding="utf-8") properties = (ROOT / "rtl/mini/formal/sysctrl_formal_props.sv").read_text(encoding="utf-8") assert "user_reset_mask" in design assert "user_core_count" in design assert "`USER_CORE_COUNT" not in properties assert "rib_wdata[4:0] < user_core_count" in properties assert "user_reset == user_reset_mask" in properties assert "SYSCTRL_TEST_STATUS_OFFSET" in properties assert "test_done" in design def test_sby_config_uses_prove_and_cover_with_bitwuzla(tmp_path: Path) -> None: design = tmp_path / "design.v" properties = tmp_path / "properties.v" prove_config = render_sby_config("bus_formal", design, properties, "bitwuzla", "prove", 20) cover_config = render_sby_config("bus_formal", design, properties, "bitwuzla", "cover", 20) compact_cover_config = render_sby_config( "i2c_formal", design, properties, "bitwuzla", "cover", 80, vcd=False ) skipped_cover_config = render_sby_config( "apu_loader_formal", design, properties, "bitwuzla", "cover", 64, skip=60 ) assert "mode prove" in prove_config assert "mode cover" in cover_config assert "depth 20" in prove_config assert "smtbmc --presat --nounroll bitwuzla" in prove_config assert f"design.v {design.resolve()}" in prove_config assert f"properties.v {properties.resolve()}" in prove_config assert "vcd off" not in cover_config assert "vcd off" in compact_cover_config assert "skip 60" in skipped_cover_config def test_bitwuzla_wrapper_translates_yosys_legacy_arguments() -> None: assert translate_arguments(["--smt2", "-i", "--seed=1"]) == ["--lang", "smt2", "--seed=1"] def test_fatfs_release_script_uses_the_locked_archive_contract() -> None: script = ROOT / "scripts/publish_fatfs_artifact.sh" content = script.read_text(encoding="utf-8") assert script.stat().st_mode & 0o111 assert 'readonly RELEASE_TAG="fatfs-r0.16"' in content assert 'readonly ASSET_NAME="fatfs-r0.16.zip"' in content assert "99f7dc1f7e095356e4a9e3dbe29959090d8b948afe2bbc5441e52fdf4b85449e" in content assert 'gh release download "$RELEASE_TAG"' in content def test_formal_result_summary_requires_every_passing_step(tmp_path: Path) -> None: proofs = ("bus", "rib2apb", "sysctrl", "pll_rcu", "gpio", "ws2812") for proof in proofs: directory = tmp_path / proof directory.mkdir() for step in ("sv2v", "prove", "cover"): (directory / f"result-{step}.json").write_text( json.dumps({"status": "passed", "tool": f"formal-{step}"}), encoding="utf-8", ) for step in ("prove", "cover"): (directory / step).mkdir() (directory / step / "status").write_text("PASS 0 1\n", encoding="utf-8") output = tmp_path / "formal.json" run( sys.executable, str(ROOT / "rtl/mini/formal/formal_results.py"), "--output", str(output), "--proof", f"bus={tmp_path / 'bus'}", "--proof", f"rib2apb={tmp_path / 'rib2apb'}", "--proof", f"sysctrl={tmp_path / 'sysctrl'}", "--proof", f"pll_rcu={tmp_path / 'pll_rcu'}", "--proof", f"gpio={tmp_path / 'gpio'}", "--proof", f"ws2812={tmp_path / 'ws2812'}", ) result = json.loads(output.read_text(encoding="utf-8")) assert result["status"] == "passed" assert set(result["proofs"]) == set(proofs) def test_mpw_generator_uses_only_the_native_v2_contract() -> None: source = (ROOT / "scripts/generate_mpw.py").read_text(encoding="utf-8") assert "migrate_user_" not in source assert "prepare_legacy_mpw_workspace" not in source assert '"mpwgen"' in source assert '"generate"' in source def test_serv_setup_patch_is_idempotent(tmp_path: Path) -> None: state = tmp_path / "serv_state.v" state.write_text( "module serv_state;\n" " wire misalign_trap_sync;\n" " assign use_trap = !trap_pending;\n" " wire trap_pending = WITH_CSR & trap_condition;\n" "endmodule\n", encoding="utf-8", ) patch_serv(tmp_path) first = state.read_text(encoding="utf-8") patch_serv(tmp_path) assert state.read_text(encoding="utf-8") == first assert first.index("wire trap_pending;") < first.index("!trap_pending") assert "assign trap_pending = WITH_CSR & trap_condition;" in first def test_serv_setup_patch_rejects_unknown_source(tmp_path: Path) -> None: state = tmp_path / "serv_state.v" state.write_text("module serv_state; endmodule\n", encoding="utf-8") with pytest.raises(RuntimeError, match="patch markers missing"): patch_serv(tmp_path) def test_mpw_active_manifest_uses_self_owned_design_selection(tmp_path: Path) -> None: manifest = tmp_path / "mpw.toml" manifest.write_text( """schema_version = 2 [generator] api_version = "retrosoc-mpw-v2" [[design]] kind = "core" id = "slow_core" slot = 0 enabled = false source_dir = "core/slow" top = "user_core_design" filelist = "usercore.fl" reset = "sync" name = "Slow" isa = "rv32i" maintainer = "owner" repo = "https://example.com/slow" [[design]] kind = "core" id = "selected_core" slot = 5 source_dir = "core/selected" top = "user_core_design" filelist = "usercore.fl" reset = "async" name = "Selected" isa = "rv32i" maintainer = "owner" repo = "https://example.com/selected" """, encoding="utf-8", ) extensions = tmp_path / "user_extensions.json" extensions.write_text( json.dumps( { "schema_version": 2, "core_targets": [ { "slot": 0, "design_id": "selected_core", "module": "mpw_c0", "reset": "async", } ], "ip_targets": [], } ), encoding="utf-8", ) active = render_active_manifest(manifest, extensions) assert 'id = "selected_core"' in active assert 'id = "slow_core"' not in active assert "slot = 0" in active assert "enabled" not in active def test_mpw_extension_bindings_match_generated_manifest(tmp_path: Path) -> None: extensions_path = tmp_path / "rtl/mini/integration/user_extensions.json" extensions_path.parent.mkdir(parents=True) extensions = { "core_targets": [ { "slot": 0, "design_id": "core_zero", "module": "mpw_core_zero", "reset": "sync", }, ], "ip_targets": [ {"slot": 1, "design_id": "ip_one", "module": "mpw_ip_one"}, ], } extensions_path.write_text(json.dumps(extensions), encoding="utf-8") output = tmp_path / "output" output.mkdir() manifest = { "designs": [ { "kind": "core", "id": "core_zero", "slot": 0, "top": "mpw_core_zero", "reset": "sync", }, {"kind": "ip", "id": "ip_one", "slot": 1, "top": "mpw_ip_one"}, ] } (output / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") validate_extension_bindings(extensions_path, output) extensions["core_targets"][0]["module"] = "wrong_core" extensions_path.write_text(json.dumps(extensions), encoding="utf-8") with pytest.raises(ValueError, match="does not match"): validate_extension_bindings(extensions_path, output) def test_prepare_norflash_and_missing_firmware(tmp_path: Path) -> None: models = tmp_path / "models" models.mkdir() for name in ("SECSI.TXT", "SFDP.TXT", "SREG.TXT"): (models / name).write_text(name, encoding="utf-8") firmware = tmp_path / "firmware.hex" firmware.write_text("00\n", encoding="utf-8") sim_dir = tmp_path / "sim" script = ROOT / "rtl/mini/script/prepare_norflash.py" run( sys.executable, str(script), "--sim-dir", str(sim_dir), "--models-dir", str(models), "--firmware", str(firmware), ) assert (sim_dir / "MEM.TXT").resolve() == firmware.resolve() assert (sim_dir / "SFDP.TXT").read_text(encoding="utf-8") == "SFDP.TXT" firmware.unlink() result = subprocess.run( [ sys.executable, str(script), "--sim-dir", str(sim_dir), "--models-dir", str(models), "--firmware", str(firmware), ], text=True, capture_output=True, ) assert result.returncode != 0 assert "firmware image not found" in result.stderr def test_dependency_helpers_are_idempotent(tmp_path: Path) -> None: source = tmp_path / "source" run("git", "init", str(source)) run("git", "config", "user.email", "test@example.com", cwd=source) run("git", "config", "user.name", "Test", cwd=source) run("git", "config", "commit.gpgsign", "false", cwd=source) (source / "dependency.txt").write_text("pinned\n", encoding="utf-8") run("git", "add", "dependency.txt", cwd=source) run("git", "commit", "-m", "pinned", cwd=source) revision = run("git", "rev-parse", "HEAD", cwd=source).stdout.strip() destination = tmp_path / "checkout" ensure_git_repo(str(source), destination, revision) first_head = run("git", "rev-parse", "HEAD", cwd=destination).stdout.strip() ensure_git_repo(str(source), destination, revision) assert run("git", "rev-parse", "HEAD", cwd=destination).stdout.strip() == first_head payload = tmp_path / "payload.bin" payload.write_bytes(b"verified") digest = hashlib.sha256(payload.read_bytes()).hexdigest() downloaded = tmp_path / "downloaded.bin" download_file(payload.as_uri(), downloaded, digest) first_mtime = downloaded.stat().st_mtime_ns download_file(payload.as_uri(), downloaded, digest) assert downloaded.stat().st_mtime_ns == first_mtime resumed = tmp_path / "resumed.bin" partial = tmp_path / ".resumed.bin.partial" partial.write_bytes(b"partial") download_file(payload.as_uri(), resumed, digest, resume=True) assert resumed.read_bytes() == b"verified" assert not partial.exists() def test_dependency_helper_limits_recursive_submodules(monkeypatch, tmp_path: Path) -> None: commands: list[tuple[str, ...]] = [] revision = "a" * 40 def record(command, *, cwd=None) -> None: del cwd commands.append(tuple(command)) def git_output_stub(repo: Path, *args: str) -> str: del repo if args == ("rev-parse", "HEAD"): return revision if args == ("status", "--porcelain"): return "" raise AssertionError(f"unexpected git command: {args}") monkeypatch.setattr(setup_helpers, "run", record) monkeypatch.setattr(setup_helpers, "git_output", git_output_stub) ensure_git_repo( "https://example.com/pdk.git", tmp_path / "pdk", revision, recursive=True, submodules=("libraries/sky130_fd_sc_hd/latest",), ) assert commands[-1] == ( "git", "submodule", "update", "--init", "--depth", "1", "--", "libraries/sky130_fd_sc_hd/latest", ) def test_make_dry_run_and_validation_do_not_write_filelists(tmp_path: Path) -> None: build_root = tmp_path / "build" def build_state() -> dict[str, tuple[int, int]]: if not build_root.exists(): return {} return { str(path.relative_to(build_root)): (path.stat().st_size, path.stat().st_mtime_ns) for path in build_root.rglob("*") if path.is_file() } before = build_state() run("make", "-n", f"BUILD_ROOT={build_root}", "help") run( "make", "-n", f"BUILD_ROOT={build_root}", f"ARCHINFO_METADATA_SCRIPT={tmp_path / 'missing-generate-metadata.py'}", "SIMU=IVERILOG", "comp", ) run( "make", "-n", f"BUILD_ROOT={build_root}", "CONFIG=configs/ci/ihp130.mk", "SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm", ) assert build_state() == before for simulator in ("UNKNOWN", "XEZIM", "CVC"): invalid = subprocess.run( ["make", f"SIMU={simulator}", "help"], cwd=ROOT, text=True, capture_output=True, ) assert invalid.returncode != 0 assert f"Invalid SIMU='{simulator}'" in invalid.stderr removed_core = subprocess.run( ["make", "CORE=alternate", "config"], cwd=ROOT, text=True, capture_output=True, ) assert removed_core.returncode != 0 assert "CORE has been removed" in removed_core.stderr default_core = run("make", "config").stdout assert any( line.startswith("MGMT_CORE") and line.rstrip().endswith("HAZARD3") for line in default_core.splitlines() ) assert any( line.startswith("MGMT_CPU_CLK_HZ") and line.rstrip().endswith("24000000") for line in default_core.splitlines() ) mpw_config = run("make", "CONFIG=configs/cluster/mini-mpw.mk", "config").stdout assert any( line.startswith("MGMT_CPU_CLK_HZ") and line.rstrip().endswith("72000000") for line in mpw_config.splitlines() ) software_makefile = (ROOT / "rtl/mini/mk/software.mk").read_text(encoding="utf-8") assert "-DRS_CPU_CLOCK_HZ=$(MGMT_CPU_CLK_HZ)U" in software_makefile removed_debug = subprocess.run( ["make", "HAVE_DEBUG=NO", "config"], cwd=ROOT, text=True, capture_output=True, ) assert removed_debug.returncode != 0 assert "HAVE_DEBUG has been removed" in removed_debug.stderr removed_ip = subprocess.run( ["make", "IP=MDD", "config"], cwd=ROOT, text=True, capture_output=True, ) assert removed_ip.returncode != 0 assert "IP is no longer configurable" in removed_ip.stderr def test_format_file_scope_is_tracked_and_self_owned() -> None: paths = [ Path("Makefile"), Path("configs/ci/example.mk"), Path("rtl/mini/top/retrosoc.sv"), Path("rtl/ip/peripheral/apb4_sysctrl.sv"), Path("rtl/tech/tc_clk.sv"), Path("rtl/demo/reference.v"), Path("tests/rtl/bus_fault_tb.sv"), Path("rtl/managed/clusterip/common/rtl/utils/register.sv"), Path("rtl/managed/third_party/core.v"), Path("rtl/mini/filelist.f"), ] assert format_files(paths, "make") == [ Path("Makefile"), Path("configs/ci/example.mk"), ] assert format_files(paths, "rtl") == [ Path("rtl/demo/reference.v"), Path("rtl/ip/peripheral/apb4_sysctrl.sv"), Path("rtl/mini/top/retrosoc.sv"), Path("rtl/tech/tc_clk.sv"), Path("tests/rtl/bus_fault_tb.sv"), ] def test_dependency_lock_and_config_key_include_a_fixed_timestamp(tmp_path: Path) -> None: lock = load_lock(ROOT / "dependencies/dependencies.lock.json") assert lock["schema_version"] == 1 assert len(lock["sources"]["mpw"]["revision"]) == 40 assert lock["sources"]["hazard3"]["destination"] == "rtl/managed/hazard3" assert lock["sources"]["pdk_sky130"]["submodules"] == ["libraries/sky130_fd_sc_hd/latest"] assert lock["container_images"]["ubuntu_22_04"]["image"] == "ubuntu" assert lock["nix_inputs"]["nixpkgs"]["revision"] == "50ab793786d9de88ee30ec4e4c24fb4236fc2674" assert lock["toolchains"]["ubuntu-22.04"]["sbt"] == { "version": "1.10.0", "url": "https://github.com/sbt/sbt/releases/download/v1.10.0/sbt-1.10.0.tgz", "sha256": "154b7de6c19207c73d0a304f901c8c4b6ead9a9c3a99a98a9d72ac19419d2640", "archive": "sbt-1.10.0.tgz", "path": "sbt/bin", "download_timeout_seconds": 600, "resume": True, } validate_flake_lock(lock, ROOT / "flake.lock") command = ( sys.executable, str(ROOT / "scripts/config_key.py"), "--lock", str(ROOT / "dependencies/dependencies.lock.json"), "--profile", "unit", "--timestamp", "2026-07-21-10-39", "--value", "ARCHITECTURE=fixed-management-and-user-extensions", "--value", "PDK=IHP130", ) first = run(*command).stdout.strip() second = run(*command).stdout.strip() assert first == second assert re.fullmatch(r"unit-2026-07-21-10-39-[0-9a-f]{12}", first) invalid_timestamp = subprocess.run( [*command, "--timestamp", "2026-07-21-10-99"], text=True, capture_output=True, ) assert invalid_timestamp.returncode != 0 assert "timestamp must use %Y-%m-%d-%H-%M" in invalid_timestamp.stderr broken = tmp_path / "broken.json" broken.write_text('{"schema_version": 1}\n', encoding="utf-8") try: load_lock(broken) except LockError as error: assert "missing or empty" in str(error) else: raise AssertionError("invalid dependency lock was accepted") invalid_submodules = json.loads(json.dumps(lock)) invalid_submodules["sources"]["pdk_sky130"]["submodules"] = ["../outside"] broken.write_text(json.dumps(invalid_submodules), encoding="utf-8") try: load_lock(broken) except LockError as error: assert "submodule" in str(error) else: raise AssertionError("invalid submodule path was accepted") invalid_timeout = json.loads(json.dumps(lock)) invalid_timeout["toolchains"]["ubuntu-22.04"]["sbt"]["download_timeout_seconds"] = 0 broken.write_text(json.dumps(invalid_timeout), encoding="utf-8") try: load_lock(broken) except LockError as error: assert "download_timeout_seconds must be positive" in str(error) else: raise AssertionError("invalid toolchain download timeout was accepted") def test_development_environment_contract_is_lock_pinned(tmp_path: Path) -> None: lock_path = ROOT / "dependencies/dependencies.lock.json" lock = load_lock(lock_path) cache = tmp_path / "development" stamp = stamp_data(ROOT, cache, DEFAULT_TOOLS, lock, lock_path) assert stamp["tools"]["verilator"] == lock["toolchains"]["ubuntu-22.04"]["verilator"]["version"] assert stamp["tools"]["openocd"] == lock["toolchains"]["ubuntu-22.04"]["openocd"]["version"] assert stamp["tools"]["sbt"] == lock["toolchains"]["ubuntu-22.04"]["sbt"]["version"] assert set(stamp["python_requirements"]) == {"requirements/build.txt", "requirements/ci.txt"} activation = render_activation(cache, [cache / "venv/bin", cache / "toolchains/verilator/bin"]) assert "export RETROSOC_DEVELOPMENT_CACHE=" in activation assert "toolchains/verilator/bin" in activation activation_path = tmp_path / "activate.sh" write_activation(activation_path, activation) assert activation_path.stat().st_mode & 0o777 == 0o644 stamp_path = tmp_path / "development-environment.json" write_stamp(stamp_path, stamp) assert json.loads(stamp_path.read_text(encoding="utf-8")) == stamp assert stamp_path.stat().st_mode & 0o777 == 0o644 def test_container_and_nix_environment_files_use_locked_inputs() -> None: lock = load_lock(ROOT / "dependencies/dependencies.lock.json") dockerfile = (ROOT / "docker/Dockerfile").read_text(encoding="utf-8") flake = (ROOT / "flake.nix").read_text(encoding="utf-8") sbom = make_sbom(lock) assert f"ubuntu@{lock['container_images']['ubuntu_22_04']['digest']}" in dockerfile assert " libncursesw6 \\\n" in dockerfile assert " libpython3.10 \\\n" in dockerfile assert " libtcl8.6 \\\n" in dockerfile assert "openjdk-17-jre-headless" in dockerfile assert " perl \\\n" in dockerfile assert " zlib1g-dev \\\n" in dockerfile assert " zlib1g \\\n" not in dockerfile assert "scripts/development_environment.py" in dockerfile assert "scripts/development_environment.py" in flake assert "buildFHSEnv" in flake assert " bzip2.out\n" in flake assert "withTermlib = true" in flake assert " expat\n" in flake assert " gmp\n" in flake assert " isl\n" in flake assert "jdk17_headless" in flake assert " libmpc\n" in flake assert " mpfr\n" in flake assert " ncursesTermlib\n" in flake assert " ncurses5\n" not in flake assert "runtimeLibraryPath = pkgs.lib.makeLibraryPath" in flake assert " pkgs.bzip2.out\n" in flake assert "LD_LIBRARY_PATH" in flake assert " perl\n" in flake assert "python310Full" in flake assert "python3Full" not in flake assert " readline\n" in flake assert "retrosoc-development retrosoc-dev" in flake assert " tcl\n" in flake assert " zlib.dev\n" in flake assert " zstd\n" in flake assert any(component["name"] == "container/ubuntu_22_04" for component in sbom["components"]) assert any(component["name"] == "nix/nixpkgs" for component in sbom["components"]) def test_development_environment_workflow_runs_the_ihp130_hosted_contract() -> None: workflow = (ROOT / ".github/workflows/development-environment.yml").read_text() assert "environment: [docker, nix]" in workflow assert "timeout-minutes: 360" in workflow assert workflow.count( "actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830" ) == 3 assert "docker build --tag retrosoc-dev-ci --file docker/Dockerfile ." in workflow assert "cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24" in workflow assert "nix flake check" in workflow assert "development_environment.py" in workflow assert '--cache "${RETROSOC_DEVELOPMENT_CACHE}" check' in workflow for command in ( "make CONFIG=configs/ci/ihp130.mk setup", "SIMU=IVERILOG SYNTH=NONE STA=NONE doctor", "SIMU=VERILATOR SYNTH=NONE STA=NONE doctor", "SIMU=IVERILOG SYNTH=YOSYS STA=NONE doctor", "SIMU=IVERILOG SYNTH=NONE STA=OPENSTA doctor", "--pdk IHP130 --behavioral-only", ): assert command in workflow.replace("\\\n", "") assert 'docker run --rm --init --platform linux/amd64' in workflow assert 'nix run .#dev -- bash -c "${IHP130_REGRESSION_COMMAND}"' in workflow assert "environment-${{ matrix.environment }}-ihp130-" in workflow def test_regression_runner_uses_one_build_timestamp(monkeypatch) -> None: monkeypatch.setenv("BUILD_TIMESTAMP", "2026-07-21-10-39") assert regression_environment()["BUILD_TIMESTAMP"] == "2026-07-21-10-39" monkeypatch.delenv("BUILD_TIMESTAMP") assert re.fullmatch( r"[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}", regression_environment()["BUILD_TIMESTAMP"], ) def test_verilator_simulations_use_uniform_timeout() -> None: verilator_makefile = ROOT / "rtl/mini/mk/verilator.mk" verilator_source = verilator_makefile.read_text(encoding="utf-8") assert "SOC_SIM_TIME ?= 180" in verilator_source assert "RTL_LINT_FLAGS := --lint-only --no-timing" in verilator_source assert "--assert --Wall" in verilator_source regression_commands = (*SMOKE_COMMANDS, *PR_COMMANDS, *NIGHTLY_COMMANDS) for _, values in regression_commands: if "SIMU=VERILATOR" in values: simulation_timeout = [value for value in values if value.startswith("SOC_SIM_TIME=")] if "APP=ci_smoke" in values and "sim" in values: assert simulation_timeout == ["SOC_SIM_TIME=1800"] assert "LINK_TYPE=ld2_all_sram" in values assert "VERILATOR_SIM_ARGS=--fast-flash" in values assert "HAVE_CSR=YES" in values elif "coremark-report" in values: assert simulation_timeout == ["SOC_SIM_TIME=7200"] assert not any(value.startswith("VERILATOR_SIM_ARGS=") for value in values) else: assert not simulation_timeout if "debug-sim" not in values: assert "HAVE_SVA=YES" in values def test_verilator_has_no_external_core_selection() -> None: command_line = (ROOT / "rtl/mini/dv/verilator/csrc/main.cpp").read_text(encoding="utf-8") wrapper = (ROOT / "rtl/mini/dv/verilator/rtl/retrosoc_top.sv").read_text(encoding="utf-8") testbench = (ROOT / "rtl/mini/dv/tb/retrosoc_tb.sv").read_text(encoding="utf-8") assert "core-sel" not in command_line assert "core_sel_i" not in wrapper assert "core_sel_i" not in testbench def test_systemverilog_testbench_starts_in_reset_with_known_clocks() -> None: testbench = (ROOT / "rtl/mini/dv/tb/retrosoc_tb.sv").read_text(encoding="utf-8") assert "localparam time ResetHoldTime = 170744ns;" in testbench for clock in ("r_ext_clk", "r_aud_clk", "r_ref24_clk"): assert re.search(rf"\b{clock}\s*=\s*1'b0;", testbench) is not None assert f"{clock} = ~{clock};" in testbench reset_init = re.search(r"\br_rst_n\s*=\s*1'b0;", testbench) assert reset_init is not None assert reset_init.start() < testbench.index("#ResetHoldTime;") assert testbench.index("#ResetHoldTime;") < testbench.index("r_rst_n = 1'b1;") assert "#43;" not in testbench def test_management_core_is_fixed_to_hazard3_with_debug() -> None: wrapper = (ROOT / "rtl/mini/top/mgmt_core_wrapper.sv").read_text(encoding="utf-8") makefile = (ROOT / "Makefile").read_text(encoding="utf-8") assert "CORE has been removed" in makefile assert "HAVE_DEBUG has been removed" in makefile assert "CORE_$(CORE)" not in makefile assert "`ifdef CORE_" not in wrapper assert "`ifdef HAVE_DEBUG" not in wrapper assert "ahbl2axi4 u_ahbl2axi4" in wrapper assert ".RESET_VECTOR (`SOC_CPU_RESET_ADDR)" in wrapper assert ".DEBUG_SUPPORT (1)" in wrapper def test_hazard3_debug_flow_is_locked_and_uses_remote_bitbang() -> None: lock = load_lock(ROOT / "dependencies/dependencies.lock.json") makefile = (ROOT / "Makefile").read_text(encoding="utf-8") wrapper = (ROOT / "rtl/mini/top/mgmt_core_wrapper.sv").read_text(encoding="utf-8") debug_wrapper = (ROOT / "rtl/mini/top/mgmt_debug_wrapper.sv").read_text(encoding="utf-8") verilator_makefile = (ROOT / "rtl/mini/mk/verilator.mk").read_text(encoding="utf-8") driver = (ROOT / "scripts/run_debug_session.py").read_text(encoding="utf-8") openocd = (ROOT / "rtl/mini/dv/verilator/openocd/retrosoc_hazard3.cfg").read_text( encoding="utf-8" ) assert "JTAG_IDCODE ?= DEADBEEF" in makefile assert "if [ $$value -gt 2147483647 ]" in makefile assert "HAVE_DEBUG ?=" not in makefile assert ".MULDIV_UNROLL (2)" in wrapper assert ".BRANCH_PREDICTOR (1)" in wrapper assert ".BREAKPOINT_TRIGGERS(2)" in wrapper assert ".HAVE_SBA(0)" in debug_wrapper assert "mgmt_debug_reset u_mgmt_debug_reset" in debug_wrapper assert "--timeout $(SOC_SIM_TIME)" in verilator_makefile assert "--require-debug-tools" in verilator_makefile assert "HAVE_DEBUG" not in verilator_makefile assert "DEBUG_GDB_PASS" in driver assert "break *0x30000008" in driver assert "adapter driver remote_bitbang" in openocd assert "catch {remote_bitbang port $jtag_port}" in openocd assert "remote_bitbang_port $jtag_port" in openocd assert "-expected-id 0xdeadbeef" in openocd assert lock["toolchains"]["ubuntu-22.04"]["openocd"]["version"] == "0.12.0-1" assert any(profile == "configs/ci/ihp130-debug.mk" for profile, _ in PR_COMMANDS) def test_benchmark_profile_uses_functional_sram_and_reserved_data() -> None: makefile = (ROOT / "Makefile").read_text(encoding="utf-8") profile = (ROOT / "configs/benchmark/ihp130-hazard3.mk").read_text(encoding="utf-8") benchmark_profile = (ROOT / "app/apps/benchmark/app.mk").read_text(encoding="utf-8") benchmark = (ROOT / "app/apps/benchmark/main.c").read_text(encoding="utf-8") assert re.search(r"^PDK_BEHAV\s+\?= NO$", makefile, re.MULTILINE) assert "PDK_BEHAV HAVE_SVA" in makefile assert "PDK_BEHAV=YES is for functional simulation" in makefile assert re.search(r"^HAVE_SRAM_MACRO\s*:= YES$", profile, re.MULTILINE) assert re.search(r"^SRAM_SIZE_KIB\s*:= 32$", profile, re.MULTILINE) assert re.search(r"^PDK_BEHAV\s*:= YES$", profile, re.MULTILINE) assert re.search(r"^SOC_SIM_TIME\s+\?= 25200$", benchmark_profile, re.MULTILINE) assert "VERILATOR_SIM_ARGS ?= --fast-flash" in benchmark_profile offset_match = re.search( r"^#define\s+RS_BENCHMARK_SRAM_OFFSET\s+UINT32_C\((0x[0-9A-F]+)\)$", benchmark, re.MULTILINE, ) words_match = re.search( r"^#define\s+RS_BENCHMARK_WORDS\s+UINT32_C\(([0-9]+)\)$", benchmark, re.MULTILINE, ) assert offset_match is not None assert words_match is not None sram_size_bytes = 32 * 1024 benchmark_bytes = int(words_match.group(1), 10) * 4 benchmark_offset = int(offset_match.group(1), 16) assert benchmark_offset + benchmark_bytes <= sram_size_bytes assert sram_size_bytes - (benchmark_offset + benchmark_bytes) >= 7 * 1024 def test_open_pdk_profiles_enable_32kib_macro_sram_and_ics55_stays_absent() -> None: for pdk in ("ihp130", "gf180", "sky130"): profile = (ROOT / f"configs/ci/{pdk}.mk").read_text(encoding="utf-8") assert re.search(r"^HAVE_SRAM_IF\s*:= YES$", profile, re.MULTILINE) assert re.search(r"^HAVE_SRAM_MACRO\s*:= YES$", profile, re.MULTILINE) assert re.search(r"^SRAM_SIZE_KIB\s*:= 32$", profile, re.MULTILINE) ics55 = (ROOT / "configs/ci/ics55.mk").read_text(encoding="utf-8") assert re.search(r"^HAVE_SRAM_IF\s*:= NO$", ics55, re.MULTILINE) assert re.search(r"^HAVE_SRAM_MACRO\s*:= NO$", ics55, re.MULTILINE) for name in ("ihp130-hazard3", "ihp130-hazard3-coremark"): benchmark = (ROOT / f"configs/benchmark/{name}.mk").read_text(encoding="utf-8") assert re.search(r"^SRAM_SIZE_KIB\s*:= 32$", benchmark, re.MULTILINE) def test_smoke_regression_uses_ihp130_behavioral_coverage_only() -> None: commands, profiles = select_regression("smoke", "IHP130") assert commands == SMOKE_COMMANDS assert profiles == () command_values = [values for _, values in commands] assert command_values[0] == RTL_LINT_VALUES assert (CI_SMOKE_APP_VALUE, "firmware") in command_values assert ("SIMU=VERILATOR", "HAVE_SVA=YES", "comp") in command_values assert ("SIMU=IVERILOG", "RTL_SIM_TIMEOUT=5200000", "sim-asm") in command_values assert not any( "synth" in values or "sta" in values or "netsim" in values for values in command_values ) dry_run = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "smoke", "--pdk", "IHP130", "--dry-run", ) assert "+ make CONFIG=configs/ci/ihp130.mk APP=ci_smoke firmware" in dry_run.stdout assert "netsim" not in dry_run.stdout invalid = subprocess.run( [ sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "smoke", "--pdk", "GF180", "--dry-run", ], text=True, capture_output=True, ) assert invalid.returncode != 0 assert "smoke regression supports only --pdk IHP130" in invalid.stderr def test_rtl_regression_excludes_synthesis_and_timing() -> None: commands, profiles = select_regression("rtl", "IHP130") assert commands == RTL_COMMANDS assert profiles == ("configs/ci/ihp130.mk",) values = [value for _, command_values in commands for value in command_values] assert "sim" in values assert "sim-asm" in values assert "debug-sim" in values assert "synth" not in values assert "netsim" not in values assert "sta" not in values dry_run = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "rtl", "--pdk", "IHP130", "--dry-run", ) assert "SIMU=VERILATOR" in dry_run.stdout assert "SIMU=IVERILOG" in dry_run.stdout assert "synth" not in dry_run.stdout assert "netsim" not in dry_run.stdout assert "STA=OPENSTA" not in dry_run.stdout def test_behavioral_only_excludes_synthesis_and_netlist_consumers() -> None: commands = behavioral_only(PR_COMMANDS) values = [value for _, command_values in commands for value in command_values] assert commands == RTL_COMMANDS assert "synth" not in values assert "sta" not in values assert "netsim" not in values dry_run = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "pr", "--pdk", "GF180", "--behavioral-only", "--dry-run", ) assert "SIMU=VERILATOR" in dry_run.stdout assert "SIMU=IVERILOG" in dry_run.stdout assert "SYNTH=YOSYS" not in dry_run.stdout assert "STA=OPENSTA" not in dry_run.stdout assert "netsim" not in dry_run.stdout nightly_extra = behavioral_only(NIGHTLY_EXTRA_COMMANDS) assert nightly_extra == NIGHTLY_EXTRA_COMMANDS[:1] def test_hosted_regression_keeps_tools_but_skips_synthesis_execution() -> None: workflow = (ROOT / ".github/workflows/_regression.yml").read_text(encoding="utf-8") assert "default: verilator sv2v iverilog yosys opensta openocd riscv_gnu" in workflow assert "SIMU=IVERILOG SYNTH=YOSYS STA=NONE doctor" in workflow assert "--behavioral-only" in workflow def test_hosted_smoke_formal_check_is_doctor_only() -> None: workflow = (ROOT / ".github/workflows/_regression.yml").read_text(encoding="utf-8") smoke = (ROOT / ".github/workflows/regression-smoke.yml").read_text(encoding="utf-8") software_makefile = (ROOT / "rtl/mini/mk/software.mk").read_text(encoding="utf-8") assert "timeout_minutes: 60" in smoke assert "formal_checks: true" in smoke assert "make CONFIG=${{ inputs.profile }} formal-doctor" in workflow assert "make CONFIG=${{ inputs.profile }} formal\n" not in workflow assert "$(DUMP) -d $(@F) > $(FIRMWARE_NAME).txt" in software_makefile assert "$(DUMP) -D" not in software_makefile def test_pdk_pr_regressions_cover_firmware_rtl_and_selected_netlist_target() -> None: assert set(PDK_PR_PROFILES) == {"GF180", "IHP130", "ICS55", "SKY130"} for pdk, profile in PDK_PR_PROFILES.items(): commands = pdk_pr_commands(profile) command_values = [values for _, values in commands] assert command_values[0] == RTL_LINT_VALUES assert (CI_SMOKE_APP_VALUE, "firmware") in command_values assert any( CI_SMOKE_APP_VALUE in values and "SIMU=VERILATOR" in values and "firmware" in values for values in command_values ) verilator_values = next(values for values in command_values if "sim" in values) if pdk == "IHP130": assert "LINK_TYPE=ld2_all_sram" in verilator_values assert "VERILATOR_SIM_ARGS=--fast-flash" in verilator_values assert "SOC_SIM_TIME=1800" in verilator_values else: assert "LINK_TYPE=ld2_sdram" in verilator_values assert "VERILATOR_SIM_ARGS=--fast-flash" in verilator_values assert "SOC_SIM_TIME=1800" in verilator_values assert "HAVE_CSR=YES" in verilator_values assert any("SIMU=IVERILOG" in values and "sim-asm" in values for values in command_values) assert any("SYNTH=YOSYS" in values and "synth" in values for values in command_values) assert not any( any(value.startswith("SYNTH_RECIPE=") for value in values) for values in command_values ) assert ("STA=OPENSTA", "sta") in command_values netlist_targets = { value for values in command_values for value in values if value in {"netsim", "netsim-boot"} } expected_target = "netsim-boot" if pdk in {"GF180", "ICS55"} else "netsim" assert netlist_targets == {expected_target} def test_nightly_regression_runs_optional_yosys_recipes() -> None: commands, profiles = select_regression("nightly", "IHP130") assert commands == NIGHTLY_COMMANDS assert profiles == ("configs/ci/ihp130.mk",) assert ( "configs/ci/ihp130.mk", ("SYNTH=YOSYS", "SYNTH_RECIPE=area", "synth"), ) in NIGHTLY_COMMANDS assert ( "configs/ci/ihp130.mk", ("STA=OPENSTA", "SYNTH_RECIPE=area", "sta"), ) in NIGHTLY_COMMANDS assert ( "configs/ci/ihp130.mk", ("SYNTH_RECIPE=area", "metrics"), ) in NIGHTLY_COMMANDS assert ( "configs/ci/ihp130.mk", ("SYNTH=YOSYS", "SYNTH_RECIPE=speed", "synth"), ) in NIGHTLY_COMMANDS assert ( "configs/ci/ihp130.mk", ("STA=OPENSTA", "SYNTH_RECIPE=speed", "sta"), ) in NIGHTLY_COMMANDS assert ( "configs/ci/ihp130.mk", ("SYNTH_RECIPE=speed", "metrics"), ) in NIGHTLY_COMMANDS assert not any( any(value.startswith("SYNTH_RECIPE=") for value in values) for _, values in PR_COMMANDS ) nightly = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "nightly", "--pdk", "IHP130", "--dry-run", ) assert ( "+ make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS SYNTH_RECIPE=area synth" in nightly.stdout ) assert "+ make CONFIG=configs/ci/ihp130.mk STA=OPENSTA SYNTH_RECIPE=area sta" in nightly.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH_RECIPE=area metrics" in nightly.stdout assert ( "+ make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS SYNTH_RECIPE=speed synth" in nightly.stdout ) assert "+ make CONFIG=configs/ci/ihp130.mk STA=OPENSTA SYNTH_RECIPE=speed sta" in nightly.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH_RECIPE=speed metrics" in nightly.stdout pr = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "pr", "--pdk", "IHP130", "--dry-run", ) assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS synth" in pr.stdout assert "SYNTH_RECIPE=area" not in pr.stdout assert "SYNTH_RECIPE=speed" not in pr.stdout def test_nightly_extra_regression_skips_pr_netsim() -> None: commands, profiles = select_regression("nightly-extra", "IHP130") assert commands == NIGHTLY_EXTRA_COMMANDS assert profiles == ("configs/ci/ihp130.mk",) assert NIGHTLY_COMMANDS == (*PR_COMMANDS, *NIGHTLY_EXTRA_COMMANDS) assert not any("netsim" in values for _, values in commands) extra = run( sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "nightly-extra", "--pdk", "IHP130", "--dry-run", ) assert ( "+ make CONFIG=configs/benchmark/ihp130-hazard3-coremark.mk " "SIMU=VERILATOR SOC_SIM_TIME=7200 HAVE_SVA=YES coremark-report" in extra.stdout ) assert "--fast-flash" not in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS SYNTH_RECIPE=area synth" in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk STA=OPENSTA SYNTH_RECIPE=area sta" in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH_RECIPE=area metrics" in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS SYNTH_RECIPE=speed synth" in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk STA=OPENSTA SYNTH_RECIPE=speed sta" in extra.stdout assert "+ make CONFIG=configs/ci/ihp130.mk SYNTH_RECIPE=speed metrics" in extra.stdout assert "netsim" not in extra.stdout invalid = subprocess.run( [ sys.executable, str(ROOT / "scripts/regress.py"), "--root", str(ROOT), "--suite", "nightly-extra", "--pdk", "GF180", "--dry-run", ], text=True, capture_output=True, check=False, ) assert invalid.returncode != 0 assert "nightly-extra regression supports only --pdk IHP130" in invalid.stderr def test_nightly_workflow_splits_netsim_from_extended_recipes() -> None: nightly = (ROOT / ".github/workflows/nightly.yml").read_text() quality = (ROOT / ".github/workflows/quality.yml").read_text() assert "suite: pr" in nightly assert "timeout_minutes: 360" in nightly assert "suite: nightly-extra" in nightly assert "timeout_minutes: 180" in nightly assert "suite: nightly\n" not in nightly assert "--suite nightly-extra --pdk IHP130 --behavioral-only --dry-run" in quality def test_quality_runs_accelerator_tests_with_locked_references_and_simulators() -> None: quality = (ROOT / ".github/workflows/quality.yml").read_text() assert "timeout-minutes: 90" in quality assert "tools: verilator sv2v iverilog yosys" in quality assert "rtl/managed/third_party" in quality assert "python3 rtl/ip/setup.py" in quality assert "make CONFIG=configs/ci/ihp130.mk setup-pdk" in quality assert "--requirement requirements/build.txt" in quality assert "make setup-apu-reference setup-apu-kws-reference setup-npu-reference" in quality for path in ( ".cache/retrosoc/sources/apu-mlperf-tiny", ".cache/retrosoc/sources/apu-kws-mfcc", ".cache/retrosoc/sources/apu-tensorflow", ".cache/retrosoc/sources/apu-gemmlowp", ".cache/retrosoc/sources/npu-vww-corpus", ".cache/retrosoc/downloads/npu", ): assert path in quality assert quality.index("python3 rtl/ip/setup.py") < quality.index( "Test scripts and RTL fixtures" ) assert quality.index("Set up locked test tools") < quality.index( "Test scripts and RTL fixtures" ) assert quality.index("Set up locked accelerator references") < quality.index( "Test scripts and RTL fixtures" ) def test_regression_observations_do_not_block_or_skip_metrics( monkeypatch, tmp_path: Path, capsys ) -> None: calls: list[list[str]] = [] monkeypatch.setattr( regress, "select_regression", lambda _suite, _pdk: ( (("unit-profile", ("rtl-lint",)),), ("unit-profile",), ), ) monkeypatch.setattr(regress, "regression_environment", lambda: {}) monkeypatch.setattr(regress, "self_owned_warnings", lambda _root, _output: []) strict_calls: list[list[str]] = [] def pass_blocking_check( command: list[str], root: Path, capture_output: bool, environment: dict[str, str] ) -> str: assert root == tmp_path assert capture_output is False assert environment == {} strict_calls.append(command) return "" monkeypatch.setattr(regress, "run_command", pass_blocking_check) def fail_quality_check( command: list[str], *, cwd: Path, env: dict[str, str], check: bool ) -> subprocess.CompletedProcess[str]: assert cwd == tmp_path assert env == {} assert check is False calls.append(command) return subprocess.CompletedProcess(command, returncode=1) monkeypatch.setattr(regress.subprocess, "run", fail_quality_check) monkeypatch.setattr( sys, "argv", ["regress.py", "--root", str(tmp_path), "--suite", "pr"], ) assert regress.main() == 0 assert strict_calls == [["make", "CONFIG=unit-profile", "rtl-lint"]] assert calls == [ [ "make", "CONFIG=unit-profile", "SIMU=VERILATOR", "HAVE_SVA=YES", "check-rtl-lint", ], ["make", "CONFIG=unit-profile", "check-warnings"], ["make", "CONFIG=unit-profile", "check-metrics"], ] assert capsys.readouterr().err.count("non-blocking observation failed") == 3 def test_rtl_lint_warning_baseline_is_independent(tmp_path: Path) -> None: profile = "unit-profile" lint_log = tmp_path / "variant/lint/verilator/lint.log" lint_log.parent.mkdir(parents=True) lint_log.write_text( f"%Warning-WIDTH: {tmp_path}/rtl/top.sv:12: width mismatch\n", encoding="utf-8", ) baseline = tmp_path / f"quality/warnings/{profile}/rtl-lint.json" run( sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "baseline", "--root", str(tmp_path), "--profile", profile, "--tool", "rtl-lint", "--log", str(lint_log), "--output", str(baseline), ) report = tmp_path / "rtl-lint-warnings.json" run( sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "check", "--root", str(tmp_path), "--profile", profile, "--variant-root", str(tmp_path / "variant"), "--tool", "rtl-lint", "--output", str(report), ) assert json.loads(report.read_text(encoding="utf-8"))["status"] == "passed" lint_log.write_text( lint_log.read_text(encoding="utf-8") + f"%Warning-UNUSED: {tmp_path}/rtl/top.sv:20: unused signal\n", encoding="utf-8", ) failed = subprocess.run( [ sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "check", "--root", str(tmp_path), "--profile", profile, "--variant-root", str(tmp_path / "variant"), "--tool", "rtl-lint", "--output", str(report), ], text=True, capture_output=True, ) assert failed.returncode != 0 assert json.loads(report.read_text(encoding="utf-8"))["failed_tools"] == ["rtl-lint"] def test_run_flow_writes_structured_result(tmp_path: Path) -> None: log = tmp_path / "flow.log" result = tmp_path / "result.json" run( sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", "unit", "--log", str(log), "--result", str(result), "--", sys.executable, "-c", "print('flow output')", ) data = json.loads(result.read_text(encoding="utf-8")) assert data["status"] == "passed" assert data["exit_code"] == 0 assert data["duration_seconds"] >= 0 assert isinstance(data["peak_rss_kib"], int) assert data["peak_rss_kib"] > 0 assert log.read_text(encoding="utf-8") == "flow output\n" def test_run_flow_can_terminate_an_opt_in_local_flow_at_success_marker(tmp_path: Path) -> None: log = tmp_path / "marker.log" result = tmp_path / "marker.json" completed = subprocess.run( [ sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", "test", "--log", str(log), "--result", str(result), "--success-marker", "Hello retroSoC!", "--terminate-on-success-marker", "--", sys.executable, "-c", "import time; print('Hello retroSoC!', flush=True); time.sleep(30)", ], text=True, capture_output=True, timeout=10, ) report = json.loads(result.read_text(encoding="utf-8")) assert completed.returncode == 0 assert report["status"] == "passed" assert report["completion_mode"] == "success_marker" assert report["success_marker_seen"] is True assert "Hello retroSoC!" in log.read_text(encoding="utf-8") def test_regression_boot_only_mode_replaces_only_netsim_command() -> None: transformed = regress.with_netsim_boot_only(PR_COMMANDS) flattened = [value for _, values in transformed for value in values] assert "netsim" not in flattened assert "netsim-boot" in flattened assert "SIM_FIRMWARE_NAME=retrosoc_asm" in flattened assert not any(value.startswith("SIM_SUCCESS_MARKER=") for value in flattened) assert any("sta" in values for _, values in transformed) def test_run_flow_adds_carriage_return_when_terminal_disables_onlcr(monkeypatch) -> None: class Terminal(io.StringIO): def isatty(self) -> bool: return True def fileno(self) -> int: return 42 terminal = Terminal() monkeypatch.setattr(run_flow.sys, "stdout", terminal) monkeypatch.setattr( run_flow.termios, "tcgetattr", lambda descriptor: [0, run_flow.termios.OPOST, 0, 0, 0, 0, 0], ) run_flow.write_console_output("first\nsecond\n") assert terminal.getvalue() == "first\r\nsecond\r\n" def test_run_flow_keeps_newlines_when_terminal_maps_them_to_carriage_return(monkeypatch) -> None: class Terminal(io.StringIO): def isatty(self) -> bool: return True def fileno(self) -> int: return 42 terminal = Terminal() monkeypatch.setattr(run_flow.sys, "stdout", terminal) monkeypatch.setattr( run_flow.termios, "tcgetattr", lambda descriptor: [0, run_flow.termios.OPOST | run_flow.termios.ONLCR, 0, 0, 0, 0, 0], ) run_flow.write_console_output("first\nsecond\n") assert terminal.getvalue() == "first\nsecond\n" def test_run_flow_byte_output_adds_carriage_return_only_for_terminal(monkeypatch) -> None: class Terminal(io.StringIO): def __init__(self) -> None: super().__init__() self.buffer = io.BytesIO() def isatty(self) -> bool: return True def fileno(self) -> int: return 42 terminal = Terminal() monkeypatch.setattr(run_flow.sys, "stdout", terminal) monkeypatch.setattr( run_flow.termios, "tcgetattr", lambda descriptor: [0, run_flow.termios.OPOST, 0, 0, 0, 0, 0], ) run_flow.write_console_bytes(b"first\nsecond\n") assert terminal.buffer.getvalue() == b"first\r\nsecond\r\n" def test_run_flow_streams_bytes_without_waiting_for_newline(tmp_path: Path) -> None: log = tmp_path / "flow.log" result = tmp_path / "result.json" process = subprocess.Popen( [ sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", "unit", "--stream-bytes", "--log", str(log), "--result", str(result), "--", sys.executable, "-c", "import os, time; os.write(1, b'A'); time.sleep(2); os.write(1, b'\\xffB')", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) assert process.stdout is not None started = time.monotonic() assert process.stdout.read(1) == b"A" assert time.monotonic() - started < 1 remaining, stderr = process.communicate(timeout=10) assert process.returncode == 0 assert stderr == b"" assert remaining == b"\xffB" assert log.read_bytes() == b"A\xffB" assert json.loads(result.read_text(encoding="utf-8"))["status"] == "passed" def test_run_flow_records_interruption(tmp_path: Path) -> None: log = tmp_path / "interrupted.log" result = tmp_path / "interrupted.json" process = subprocess.Popen( [ sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", "unit", "--log", str(log), "--result", str(result), "--", sys.executable, "-c", "import time; time.sleep(30)", ], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) for _ in range(50): if log.exists(): break time.sleep(0.02) process.send_signal(signal.SIGINT) process.communicate(timeout=10) assert process.returncode == 130 data = json.loads(result.read_text(encoding="utf-8")) assert data["status"] == "interrupted" assert data["exit_code"] == 130 assert data["error"] == "interrupted" def test_simulation_success_marker_and_failure_detection(tmp_path: Path) -> None: log = tmp_path / "sim.log" result = tmp_path / "result.json" log.write_text( "SIM_TEST_PASS code=0\nSimulation complete\n", encoding="utf-8", ) run( sys.executable, str(ROOT / "scripts/check_simulation.py"), "--log", str(log), "--result", str(result), ) assert json.loads(result.read_text(encoding="utf-8"))["status"] == "passed" log.write_text( "SIM_TEST_PASS code=0\nSIM_TEST_FAIL code=1\n", encoding="utf-8", ) failed = subprocess.run( [ sys.executable, str(ROOT / "scripts/check_simulation.py"), "--log", str(log), "--result", str(result), ], text=True, capture_output=True, ) assert failed.returncode != 0 assert json.loads(result.read_text(encoding="utf-8"))["status"] == "failed" def test_warning_baseline_rejects_new_signature(tmp_path: Path) -> None: profile = "unit-profile" log = tmp_path / "variant/sim/verilator/verilating.log" log.parent.mkdir(parents=True) log.write_text( f"%Warning-WIDTH: {tmp_path}/rtl/top.sv:12: width mismatch\n", encoding="utf-8", ) baseline = tmp_path / f"quality/warnings/{profile}/verilator.json" run( sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "baseline", "--root", str(tmp_path), "--profile", profile, "--tool", "verilator", "--log", str(log), "--output", str(baseline), ) report = tmp_path / "warnings.json" run( sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "check", "--root", str(tmp_path), "--profile", profile, "--variant-root", str(tmp_path / "variant"), "--tool", "verilator", "--output", str(report), ) log.write_text( log.read_text(encoding="utf-8") + f"%Warning-UNUSED: {tmp_path}/rtl/top.sv:20: unused signal\n", encoding="utf-8", ) failed = subprocess.run( [ sys.executable, str(ROOT / "scripts/analyze_warnings.py"), "check", "--root", str(tmp_path), "--profile", profile, "--variant-root", str(tmp_path / "variant"), "--tool", "verilator", "--output", str(report), ], text=True, capture_output=True, ) assert failed.returncode != 0 assert json.loads(report.read_text(encoding="utf-8"))["failed_tools"] == ["verilator"] def test_warning_normalization_keeps_ranges_and_removes_variant_hash(tmp_path: Path) -> None: message = ( f"{tmp_path}/build/profile-deadbeef/generated/core.sv:42: " "Bit extraction of var[7:0] is too wide" ) normalized = normalize(tmp_path, "WIDTH", message) assert normalized == ( "WIDTH:$BUILD/generated/core.sv:: Bit extraction of var[7:0] is too wide" ) def test_warning_normalization_preserves_pdk_signatures(tmp_path: Path) -> None: message = ( f"{tmp_path}/physical/pdk/IHP-Open-PDK/ihp-sg13g2/" "libs.ref/sg13g2_stdcell/verilog/sg13g2_stdcell.v:42: " "Ignoring unsupported specify block" ) normalized = normalize(tmp_path, "SPECIFYIGN", message) assert normalized == ( "SPECIFYIGN:$ROOT/pdk/IHP-Open-PDK/ihp-sg13g2/" "libs.ref/sg13g2_stdcell/verilog/sg13g2_stdcell.v:: " "Ignoring unsupported specify block" ) def test_warning_normalization_maps_isolated_mpw_sources_to_managed_sources( tmp_path: Path, ) -> None: message = ( f"{tmp_path}/build/ihp130-deadbeef/generated/mpw/verilator/core/username1/" "./kianV/kianv_harris_mc_edition_username1.v:42: unused signal" ) normalized = normalize(tmp_path, "UNUSEDSIGNAL", message) assert normalized == ( "UNUSEDSIGNAL:$ROOT/rtl/managed/mpw/core/username1/kianV/" "kianv_harris_mc_edition.v:: unused signal" ) def test_c_warning_filter_excludes_vendored_sources(tmp_path: Path) -> None: policy_dir = tmp_path / "quality" policy_dir.mkdir() (policy_dir / "embedded_c_policy.json").write_text( (ROOT / "quality/embedded_c_policy.json").read_text(encoding="utf-8"), encoding="utf-8", ) output = "\n".join( ( f"{tmp_path}/crt/src/hal/timer.c:9:1: warning: self-owned warning", f"{tmp_path}/app/coremark/coremark-main/core_main.c:4:1: warning: vendor warning", ) ) warnings = self_owned_warnings(tmp_path, output) assert warnings == ["crt/src/hal/timer.c:9: warning: self-owned warning"] def test_metrics_collection_and_observe_policy(tmp_path: Path) -> None: variant = tmp_path / "variant" (variant / "sw").mkdir(parents=True) (variant / "sw/firmware.bin").write_bytes(b"1234") report_dir = variant / "syn/yosys/rpt" report_dir.mkdir(parents=True) (report_dir / "retrosoc_asic_area.rpt").write_text( " 42 1.23E+02 retrosoc_asic\nChip area for top module '\\retrosoc_asic': 123.0\n", encoding="utf-8", ) (report_dir / "retrosoc_asic_area.json").write_text( json.dumps({"design": {"area": 124.0, "num_cells": 43}}), encoding="utf-8", ) timing_dir = variant / "sta/opensta" timing_dir.mkdir(parents=True) (timing_dir / "timing_metrics.rpt").write_text("-1.0\n-2.0\n-3.0\n-4.0\n", encoding="utf-8") (variant / "result-unit.json").write_text( json.dumps({"status": "passed", "duration_seconds": 1.25}), encoding="utf-8", ) metrics = tmp_path / "metrics.json" run( sys.executable, str(ROOT / "scripts/metrics.py"), "collect", "--variant-root", str(variant), "--output", str(metrics), ) data = json.loads(metrics.read_text(encoding="utf-8")) assert data["firmware"]["firmware.bin"]["bytes"] == 4 assert data["synthesis"] == {"recipe": "balanced", "top_area": 124.0, "top_cells": 43} assert data["timing"]["wns_min"] == -1.0 policy = tmp_path / "policy.json" policy.write_text('{"mode": "observe"}\n', encoding="utf-8") run( sys.executable, str(ROOT / "scripts/metrics.py"), "check", "--metrics", str(metrics), "--policy", str(policy), ) def test_metrics_collection_selects_recipe_roots(tmp_path: Path) -> None: variant = tmp_path / "variant" synth_root = variant / "syn/yosys-area" sta_root = variant / "sta/opensta-area" (synth_root / "rpt").mkdir(parents=True) sta_root.mkdir(parents=True) (synth_root / "rpt/retrosoc_asic_area.json").write_text( json.dumps({"design": {"area": 88.0, "num_cells": 19}}), encoding="utf-8" ) (sta_root / "timing_metrics.rpt").write_text("wns_max=-0.25\n", encoding="utf-8") metrics = tmp_path / "metrics-area.json" run( sys.executable, str(ROOT / "scripts/metrics.py"), "collect", "--variant-root", str(variant), "--synth-root", str(synth_root), "--sta-root", str(sta_root), "--recipe", "area", "--output", str(metrics), ) data = json.loads(metrics.read_text(encoding="utf-8")) assert data["schema_version"] == 2 assert data["synthesis"] == {"recipe": "area", "top_area": 88.0, "top_cells": 19} assert data["timing"]["wns_max"] == -0.25 def test_safe_extract_rejects_parent_traversal(tmp_path: Path) -> None: archive = tmp_path / "unsafe.tar" with tarfile.open(archive, "w") as bundle: member = tarfile.TarInfo("../outside") payload = b"unsafe" member.size = len(payload) bundle.addfile(member, io.BytesIO(payload)) try: safe_extract(archive, tmp_path / "output") except ValueError as error: assert "unsafe archive member" in str(error) else: raise AssertionError("unsafe archive was extracted") assert not (tmp_path / "outside").exists() def test_toolchain_tree_is_accessible_to_runtime_users(tmp_path: Path) -> None: toolchain = tmp_path / "toolchain" binary_directory = toolchain / "bundle/bin" binary_directory.mkdir(parents=True) executable = binary_directory / "tool" executable.write_text("#!/bin/sh\n", encoding="utf-8") data = toolchain / "bundle/data" data.write_text("public tool data\n", encoding="utf-8") marker = toolchain / ".complete" marker.write_text("digest\n", encoding="utf-8") toolchain.chmod(0o700) (toolchain / "bundle").chmod(0o700) binary_directory.chmod(0o700) executable.chmod(0o700) data.chmod(0o600) marker.chmod(0o600) make_tree_world_readable(toolchain) assert toolchain.stat().st_mode & 0o777 == 0o755 assert (toolchain / "bundle").stat().st_mode & 0o777 == 0o755 assert binary_directory.stat().st_mode & 0o777 == 0o755 assert executable.stat().st_mode & 0o777 == 0o755 assert data.stat().st_mode & 0o777 == 0o644 assert marker.stat().st_mode & 0o777 == 0o644 def test_fatfs_update_reextracts_downloaded_archive(tmp_path: Path) -> None: module_path = ROOT / "app/setup.py" spec = importlib.util.spec_from_file_location("retrosoc_app_setup", module_path) assert spec is not None and spec.loader is not None app_setup = importlib.util.module_from_spec(spec) spec.loader.exec_module(app_setup) archive = tmp_path / "ff16.zip" with ZipFile(archive, "w") as zip_archive: zip_archive.writestr("source/ff.c", "new archive contents\n") source = tmp_path / "source" source.mkdir() (source / "ff.c").write_text("stale contents\n", encoding="utf-8") app_setup.extract_fatfs(archive, source, update=False) assert (source / "ff.c").read_text(encoding="utf-8") == "stale contents\n" app_setup.extract_fatfs(archive, source, update=True) assert (source / "ff.c").read_text(encoding="utf-8") == "new archive contents\n" def test_coremark_setup_patch_is_idempotent(tmp_path: Path) -> None: module_path = ROOT / "app/setup.py" spec = importlib.util.spec_from_file_location("retrosoc_app_setup", module_path) assert spec is not None and spec.loader is not None app_setup = importlib.util.module_from_spec(spec) spec.loader.exec_module(app_setup) coremark = tmp_path / "coremark" coremark.mkdir() (coremark / "coremark.h").write_text('#include "core_portme.h"\n', encoding="utf-8") (coremark / "core_main.c").write_text( "/* Function: main */\nmain(void) {\n" " if (time_in_secs(total_time) < 10) {\n }\n" " return MAIN_RETURN_VAL;\n}\n", encoding="utf-8", ) previous_coremark_dir = app_setup.COREMARK_DIR try: app_setup.COREMARK_DIR = coremark app_setup.patch_coremark() first = (coremark / "core_main.c").read_text(encoding="utf-8") app_setup.patch_coremark() second = (coremark / "core_main.c").read_text(encoding="utf-8") finally: app_setup.COREMARK_DIR = previous_coremark_dir assert first == second assert "core_main(void)" in first assert "COREMARK_MIN_RUN_SECS" in first assert "#if COREMARK_MIN_RUN_SECS > 0" in first assert "return total_errors == 0 ? 0 : 1;" in first def test_ci_actions_are_pinned_to_commits() -> None: action_files = [ *sorted((ROOT / ".github/workflows").glob("*.yml")), *sorted((ROOT / ".github/actions").glob("*/action.yml")), ] remote_uses = [] for path in action_files: for line in path.read_text(encoding="utf-8").splitlines(): match = re.match(r"\s*-?\s*uses:\s*([^\s#]+)", line) if match and not match.group(1).startswith("./"): remote_uses.append((path, match.group(1))) assert remote_uses for path, value in remote_uses: assert re.fullmatch(r"[^@]+@[0-9a-f]{40}", value), f"unpinned action in {path}: {value}" HASH_LOCKED_REQUIREMENT = re.compile(r"[^\s=]+==[^\s]+(?:\s+--hash=sha256:[0-9a-f]{64})+") def hash_locked_requirement_lines(content: str) -> list[str]: logical_content = re.sub(r"\\\r?\n[ \t]*", " ", content) return [ line.strip() for line in logical_content.splitlines() if line.strip() and not line.lstrip().startswith(("#", "--")) ] def test_python_requirements_are_hash_locked() -> None: for path in sorted((ROOT / "requirements").glob("*.txt")): content = path.read_text(encoding="utf-8") assert "--require-hashes" in content assert "--only-binary=:all:" in content requirements = hash_locked_requirement_lines(content) assert requirements for requirement in requirements: assert HASH_LOCKED_REQUIREMENT.fullmatch(requirement), ( f"unlocked requirement in {path}: {requirement}" ) def test_python_requirement_hash_validation_accepts_multiple_hashes() -> None: first_hash = "a" * 64 second_hash = "b" * 64 requirements = hash_locked_requirement_lines( "--only-binary=:all:\n" "--require-hashes\n" "ruff==0.15.21 \\\n" f" --hash=sha256:{first_hash} \\\n" f" --hash=sha256:{second_hash}\n" ) assert len(requirements) == 1 assert HASH_LOCKED_REQUIREMENT.fullmatch(requirements[0]) def test_python_requirement_hash_validation_rejects_unlocked_entries() -> None: valid_hash = "a" * 64 invalid_requirements = ( "ruff>=0.15.21 --hash=sha256:" + valid_hash, "ruff==0.15.21", "ruff==0.15.21 --hash=sha256:" + "a" * 63, "ruff==0.15.21 --hash=sha256:" + valid_hash + " --extra", ) for requirement in invalid_requirements: assert not HASH_LOCKED_REQUIREMENT.fullmatch(requirement) def test_clean_all_stays_within_repository(tmp_path: Path) -> None: fake_root = tmp_path / "repository" (fake_root / ".git").mkdir(parents=True) (fake_root / "Makefile").write_text("all:\n", encoding="utf-8") generated = fake_root / "rtl/mini/.iverilog_build" generated.mkdir(parents=True) (generated / "simv").write_text("generated", encoding="utf-8") dependency = fake_root / "rtl/managed/third_party" dependency.mkdir(parents=True) (dependency / "model.v").write_text("dependency", encoding="utf-8") run( sys.executable, str(ROOT / "scripts/clean.py"), "--root", str(fake_root), "--path", str(generated), ) assert not generated.exists() assert (dependency / "model.v").is_file() outside = subprocess.run( [ sys.executable, str(ROOT / "scripts/clean.py"), "--root", str(fake_root), "--path", str(tmp_path / "outside"), ], text=True, capture_output=True, ) assert outside.returncode != 0 external = tmp_path / "external" external.mkdir() link = fake_root / "build-link" link.symlink_to(external, target_is_directory=True) run( sys.executable, str(ROOT / "scripts/clean.py"), "--root", str(fake_root), "--path", str(link), ) assert not link.exists() assert external.is_dir()