Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""End to end: generate a project, then check KiCad agrees with Boxsim.
|
|
|
|
The netlist comparison is the real gate, so it gets its own teeth test: a
|
|
deliberately mis-wired schematic must be reported as a mismatch. A gate that
|
|
has never been shown to fail is decoration.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from boxsim import project as project_mod
|
|
from boxsim import verify
|
|
from boxsim.schematic import DEFAULT_PITCH_MM, SchematicBuilder, transform
|
|
|
|
|
|
def test_rotation_transform_matches_kicad():
|
|
# Derived empirically from KiCad's own demo schematics.
|
|
assert transform(0, 3.81, 2.54) == (3.81, -2.54)
|
|
assert transform(90, 3.81, 2.54) == (-2.54, -3.81)
|
|
assert transform(180, 3.81, 2.54) == (-3.81, 2.54)
|
|
assert transform(270, 3.81, 2.54) == (2.54, 3.81)
|
|
|
|
|
|
def test_pitch_must_clear_widest_symbol(library):
|
|
with pytest.raises(ValueError, match="widest symbol"):
|
|
SchematicBuilder(library, pitch=20.0)
|
|
SchematicBuilder(library, pitch=DEFAULT_PITCH_MM) # must not raise
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def built(tmp_path_factory, default_project, catalog, library):
|
|
outdir = tmp_path_factory.mktemp("kicad")
|
|
return project_mod.build(default_project, "Default", outdir, catalog, library)
|
|
|
|
|
|
def test_project_files_written(built):
|
|
assert built.schematic.exists()
|
|
assert built.project_file.exists()
|
|
assert (built.directory / "sym-lib-table").exists()
|
|
assert (built.directory / "fp-lib-table").exists()
|
|
text = built.schematic.read_text(encoding="utf-8")
|
|
assert text.startswith("(kicad_sch")
|
|
assert "(version 20260101)" in text
|
|
assert "extends" not in text, "derived symbols must be flattened"
|
|
|
|
|
|
def test_all_components_placed(built, default_project):
|
|
assert len(built.sheet.placements) == len(default_project.crossovers[0].components)
|
|
refdes = [p.refdes for p in built.sheet.placements]
|
|
assert len(set(refdes)) == len(refdes), "reference designators must be unique"
|
|
assert sum(1 for r in refdes if r.startswith("LS")) == 3
|
|
assert sum(1 for r in refdes if r.startswith("J")) == 1
|
|
|
|
|
|
def test_output_is_deterministic(tmp_path, default_project, catalog, library):
|
|
a = project_mod.build(default_project, "Default", tmp_path / "a", catalog, library)
|
|
b = project_mod.build(default_project, "Default", tmp_path / "b", catalog, library)
|
|
assert a.schematic.read_text() == b.schematic.read_text(), (
|
|
"UUIDs must be derived, not random, so regeneration is reproducible"
|
|
)
|
|
|
|
|
|
@pytest.mark.needs_kicad
|
|
def test_gate_passes(built, default_project, kicad_cli):
|
|
gate = verify.run_gate(
|
|
built.schematic, default_project.crossovers[0], built.sheet.placements
|
|
)
|
|
assert gate.erc_ok, gate.messages
|
|
assert gate.netlist_matches, gate.messages
|
|
assert len(gate.reference_nets) == 14
|
|
|
|
|
|
@pytest.mark.needs_kicad
|
|
def test_gate_has_teeth(tmp_path, default_project, catalog, library, kicad_cli):
|
|
"""Swap one component's terminals and confirm the gate reports it.
|
|
|
|
The reference netlist is derived from the Boxsim grid while the compared one
|
|
is extracted from the sheet by KiCad, so mis-wiring a single part must break
|
|
the comparison. If this ever passes, the gate proves nothing.
|
|
"""
|
|
result = project_mod.build(
|
|
default_project, "Broken", tmp_path / "broken", catalog, library
|
|
)
|
|
crossover = default_project.crossovers[0]
|
|
|
|
# Pick a component whose two terminals are on different nets, then lie about
|
|
# which grid node one of its pins reaches.
|
|
netlist = crossover.build_netlist()
|
|
victim = next(
|
|
p
|
|
for p in result.sheet.placements
|
|
if len({netlist.net_of(n) for n in p.pin_nodes.values()}) == 2
|
|
)
|
|
pins = sorted(victim.pin_nodes)
|
|
victim.pin_nodes[pins[0]], victim.pin_nodes[pins[1]] = (
|
|
victim.pin_nodes[pins[1]],
|
|
victim.pin_nodes[pins[0]],
|
|
)
|
|
|
|
gate = verify.run_gate(result.schematic, crossover, result.sheet.placements)
|
|
assert not gate.netlist_matches, (
|
|
"a swapped terminal was not detected: the netlist comparison is not "
|
|
"actually comparing anything"
|
|
)
|
|
assert not gate.ok
|
|
assert any("mismatch" in m for m in gate.messages)
|