"""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)