engineering/mech/ opens with the four modules every track-B/G branch imports, so the four worktrees can be cut from this commit and none of them has to invent a name the others will conflict with. - primitives.py: the typed descriptions the pure modules emit (Line, Arc, Circle, Poly, HatchArea, Text, DimIntent) with bbox / translate / rotate / scale, and ROLE_LAYER - the one place an ISO 128 line role becomes a layer name, so no module hardcodes a layer. The arc bbox sweeps its quadrants: a 0-180 arc reports y=+r, not its endpoints, which is what keeps the view layout from stacking two views on top of each other. - xdata.py: the ACADMCP_MECH codec in Track A's shape - a 1001 application marker, 1000 chunks of at most 255 ASCII characters, the 16 KB per-entity ceiling enforced before anything is written, and a foreign application, a corrupt chunk stream, a wrong version or an unknown kind refused with ValueError instead of decoded into something plausible. - standards/__init__.py: the table registry. A size outside the coverage is refused against the coverage; a size inside it that is not a listed row is refused against the rows. Nothing is interpolated, an ambiguous variant is never resolved by taking the first row, and a registration whose SOURCE does not name the table it was transcribed from is refused. - standards/materials.py: the section-hatch map for nine materials. ISO 128-50 standardises only the general hatching (narrow continuous lines at 45 degrees) - that is acadiso.pat ANSI31 and what engineering/gear.py already draws for steel. The distinguishing symbols are ASME Y14.2M's section lining as shipped in AutoCAD 2026's acadiso.pat, read off the installed file; cast iron's departure from ASME and the absence of any mechanical symbol for concrete and wood are stated in the rows themselves. Angles are 0.0 because the patterns already carry 45 degrees, and the two architectural patterns' scales are derived from the ANSI31 line spacing and recomputed by the test rather than chosen. No tool is registered here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
95 lines
2.8 KiB
Python
95 lines
2.8 KiB
Python
"""The standards registry: one row or a named refusal, never an interpolation."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import engineering.mech.standards as standards_mod
|
|
from engineering.mech.standards import (
|
|
Coverage,
|
|
coverage_of,
|
|
lookup,
|
|
register,
|
|
source_of,
|
|
standards,
|
|
)
|
|
|
|
SOURCE = "TEST 1 (2026), table 1 — fixture rows, not a real standard"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _fixture_tables():
|
|
"""Swap the process-wide registry for three fixture tables, then restore it."""
|
|
saved = dict(standards_mod._REGISTRY)
|
|
standards_mod._REGISTRY.clear()
|
|
register(
|
|
"TEST 1",
|
|
{10: {"d": 10.0, "w": 3.0}, 20: {"d": 20.0, "w": 6.0}},
|
|
Coverage(10.0, 20.0),
|
|
SOURCE,
|
|
)
|
|
register(
|
|
"TEST 2",
|
|
{18: ({"form": "E", "r": 0.6}, {"form": "F", "r": 0.8})},
|
|
Coverage(18.0, 18.0),
|
|
SOURCE,
|
|
)
|
|
register("TEST 3", {"M12": {"pitch": 1.75}}, Coverage(12.0, 12.0), SOURCE)
|
|
yield
|
|
standards_mod._REGISTRY.clear()
|
|
standards_mod._REGISTRY.update(saved)
|
|
|
|
|
|
def test_registered_standards_are_listed_and_carry_their_source():
|
|
assert standards() == ("TEST 1", "TEST 2", "TEST 3")
|
|
assert source_of("TEST 1") == SOURCE
|
|
assert coverage_of("TEST 1") == Coverage(10.0, 20.0, "mm")
|
|
|
|
|
|
def test_a_listed_size_returns_a_copy_of_its_row():
|
|
row = lookup("TEST 1", 10)
|
|
assert row == {"d": 10.0, "w": 3.0}
|
|
row["w"] = 99.0
|
|
assert lookup("TEST 1", 10)["w"] == 3.0
|
|
|
|
|
|
def test_a_size_outside_the_coverage_is_refused_by_name():
|
|
with pytest.raises(ValueError, match="TEST 1 covers 10-20 mm; 95 mm is outside the table"):
|
|
lookup("TEST 1", 95)
|
|
|
|
|
|
def test_a_size_inside_the_coverage_but_not_in_the_table_is_never_interpolated():
|
|
with pytest.raises(ValueError, match="no row for 15"):
|
|
lookup("TEST 1", 15)
|
|
|
|
|
|
def test_a_variant_is_selected_by_keyword():
|
|
assert lookup("TEST 2", 18, form="E")["r"] == 0.6
|
|
assert lookup("TEST 2", 18, form="F")["r"] == 0.8
|
|
|
|
|
|
def test_an_unknown_variant_is_refused_and_an_ambiguous_one_too():
|
|
with pytest.raises(ValueError, match="no form='G' row"):
|
|
lookup("TEST 2", 18, form="G")
|
|
with pytest.raises(ValueError, match="ambiguous"):
|
|
lookup("TEST 2", 18)
|
|
|
|
|
|
def test_a_designation_key_is_case_insensitive():
|
|
assert lookup("TEST 3", "m12")["pitch"] == 1.75
|
|
|
|
|
|
def test_an_unregistered_standard_names_the_ones_that_exist():
|
|
with pytest.raises(ValueError, match="DIN 9999.*TEST 1"):
|
|
lookup("DIN 9999", 10)
|
|
|
|
|
|
def test_a_source_that_does_not_name_its_table_is_refused():
|
|
with pytest.raises(ValueError, match="SOURCE"):
|
|
register("TEST 4", {1: {"a": 1}}, Coverage(1.0, 1.0), "made up")
|
|
|
|
|
|
def test_registering_the_same_standard_twice_is_refused():
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
register("TEST 1", {10: {"d": 10.0}}, Coverage(10.0, 10.0), SOURCE)
|