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)