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>
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from boxsim import symbols
|
|
from boxsim.catalog import SERIES_TO_SYMBOL
|
|
from boxsim.model import Orientation, Part, PartType
|
|
|
|
|
|
def test_catalogue_shape(catalog):
|
|
"""Guard the figures the resolver design relies on."""
|
|
assert len(catalog) == 95
|
|
assert len(catalog.series_names) == 11
|
|
assert set(catalog.series_names) == set(SERIES_TO_SYMBOL)
|
|
# The last catalogue line is a real part; the file has no trailing newline
|
|
# and is easy to truncate by one.
|
|
assert catalog.find("SP 1,3mm", 2.2e-3) is not None
|
|
|
|
|
|
def test_every_catalogue_part_has_a_symbol(catalog, library):
|
|
extras = symbols.cross_check(catalog, library)
|
|
assert extras == ["MKT 22μF"], (
|
|
"library should contain exactly the catalogue plus Amplifier, Speaker "
|
|
"and the radial MKT 22uF that Boxsim cannot select"
|
|
)
|
|
|
|
|
|
def test_library_values_are_matched_numerically(library):
|
|
"""Irregular library names must still resolve: 1.0uF next to 10uF."""
|
|
assert library.lookup("MKP", 1e-6) is not None
|
|
assert library.lookup("MKP", 10e-6) is not None
|
|
assert library.lookup("SP-Spule", 1e-3, "0.6").name == "SP-Spule 1.0mH (0.6)"
|
|
assert library.lookup("LR-Spule", 10e-3).name == "LR-Spule 10mH"
|
|
|
|
|
|
def test_derived_symbols_resolve_geometry(library):
|
|
"""Most symbols use `extends`; pins live on the base symbol."""
|
|
from boxsim.schematic import symbol_pins
|
|
|
|
pins = symbol_pins(library, "SP-Spule 0.82mH (0.6)")
|
|
assert len(pins.by_number) == 2
|
|
assert library.base_of("SP-Spule 0.82mH (0.6)") == "FC-Spule 10mH"
|
|
|
|
|
|
def test_flattened_definition_has_no_extends(library):
|
|
body = library.definition("SP-Spule 0.82mH (0.6)")
|
|
assert "extends" not in body
|
|
assert '(symbol "SP-Spule 0.82mH (0.6)_' in body, "graphics must be inlined"
|
|
|
|
|
|
def test_default_project_fully_resolves(catalog, library, default_project):
|
|
resolver = symbols.Resolver(catalog, library)
|
|
components = default_project.crossovers[0].components
|
|
results = resolver.resolve_all(components)
|
|
assert len(results) == len(components) == 22
|
|
assert all(r.symbol in library for r in results)
|
|
|
|
|
|
def test_empty_series_is_inferred_from_sibling(catalog, library, default_project):
|
|
resolver = symbols.Resolver(catalog, library)
|
|
components = default_project.crossovers[0].components
|
|
results = {r.part.slot: r for r in resolver.resolve_all(components)}
|
|
part231 = results[231]
|
|
assert part231.inferred_series is True
|
|
assert part231.symbol == "MKT-A 15μF", "sibling Part198 is an MKT-A 15uF"
|
|
|
|
|
|
def test_dcr_deviation_is_reported_not_corrected(catalog, library, default_project):
|
|
resolver = symbols.Resolver(catalog, library)
|
|
results = {
|
|
r.part.slot: r
|
|
for r in resolver.resolve_all(default_project.crossovers[0].components)
|
|
}
|
|
assert results[5].dcr_note is not None, "0.28 vs catalogue 0.33"
|
|
assert results[48].dcr_note is not None, "5 vs catalogue 4.8"
|
|
assert results[5].part.dcr == 0.28, "the project value must win"
|
|
|
|
|
|
def test_unstocked_value_is_a_hard_error(catalog, library):
|
|
resolver = symbols.Resolver(catalog, library)
|
|
part = Part(
|
|
slot=1,
|
|
typ=PartType.CAPACITOR,
|
|
orientation=Orientation.HORIZONTAL,
|
|
row=0,
|
|
col=0,
|
|
node_a=0,
|
|
node_b=1,
|
|
value=13e-6, # not a Visaton value
|
|
label="MKT-A",
|
|
)
|
|
with pytest.raises(symbols.SymbolError, match="not stocked"):
|
|
resolver.resolve(part)
|
|
|
|
|
|
def test_unknown_series_is_a_hard_error(catalog, library):
|
|
resolver = symbols.Resolver(catalog, library)
|
|
part = Part(
|
|
slot=2,
|
|
typ=PartType.CAPACITOR,
|
|
orientation=Orientation.HORIZONTAL,
|
|
row=0,
|
|
col=0,
|
|
node_a=0,
|
|
node_b=1,
|
|
value=10e-6,
|
|
label="Mundorf Supreme",
|
|
)
|
|
with pytest.raises(symbols.SymbolError, match="not in the Visaton catalogue"):
|
|
resolver.resolve(part)
|
|
|
|
|
|
def test_catalogue_shape_check_has_teeth(catalog):
|
|
"""A truncated catalogue must be rejected, not silently accepted.
|
|
|
|
`cross_check` only proves the catalogue is a subset of the library, so a
|
|
short catalogue passes it and then rejects valid projects at runtime with
|
|
"not stocked by Visaton". This is the check that catches it.
|
|
"""
|
|
from boxsim import catalog as catalog_mod
|
|
|
|
catalog_mod.check_shape(catalog) # the real thing must pass
|
|
|
|
short = catalog_mod.Catalog(catalog.parts[:40])
|
|
with pytest.raises(catalog_mod.CatalogError, match="expected"):
|
|
catalog_mod.check_shape(short)
|
|
|
|
# Losing a whole series must also be caught, even at the right total.
|
|
without_sp13 = catalog_mod.Catalog(
|
|
[p for p in catalog.parts if p.series != "SP 1,3mm"]
|
|
)
|
|
with pytest.raises(catalog_mod.CatalogError):
|
|
catalog_mod.check_shape(without_sp13)
|