Files
fastengen/tests/test_validate_geom.py
OpenClaw Coder e407d59b13
CI / quality (push) Canceled after 0s
CI / test (3.10) (push) Canceled after 0s
CI / test (3.11) (push) Canceled after 0s
CI / test (3.12) (push) Canceled after 0s
CI / build-check (push) Canceled after 0s
chore(P4): ruff lint, coverage/CI, README overhaul
- ruff profile in pyproject (E/W/F/I/B/RUF); clean src/ + tests/
- remove dead code: threaded_len/s/d/coat/first_rec_for_size, self.cat in gui
- 59 public re-exports added to __all__ (sorted)
- renamed l->ln (E741), zip(strict=True), split long lines, [*list, x] (RUF005)
- CI: quality job (ruff), test runs tests/ -k 'not gen_all' w/ coverage
- README rewritten (commands, API modules, tests/lint/coverage, CI)
- docs/tasks.md: P4 recorded
- 136 passed, 3 deselected (regen); ruff: all checks passed
2026-11-11 16:30:07 +10:00

119 lines
3.8 KiB
Python

"""Validation of generated STEP models against GOST nominal dimensions.
Pattern from PartCAD / cq_warehouse: a part family is validated by asserting the
generated geometry reproduces the nominal key dimensions of the standard
(across-flats S, outer diameter D, head height k) within a relative tolerance.
Requires cadquery + OCC (STEP reading). Skipped if unavailable.
No em-dash in code: Python 3.10 source lacks UTF-8 mode (no PEP 540).
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import pytest
try:
import cadquery as cq
from fastengen.model import (
generate_bolt,
generate_nut,
generate_washer,
)
from fastengen.standard import (
BOLT_SIZES,
NUT_SIZES,
WASHER_SIZES,
get_bolt,
get_nut,
get_washer,
)
from fastengen.validate_geom import DimensionResult, validate_model
HAS_CQ = True
except ImportError:
HAS_CQ = False
pytestmark = pytest.mark.skipif(not HAS_CQ, reason="requires cadquery")
def _gen_bolt(tmp_path, size, length):
path = os.path.join(str(tmp_path), f"bolt_{size}_{length}.step")
generate_bolt(get_bolt(size, length), path)
return path
def _gen_nut(tmp_path, size):
path = os.path.join(str(tmp_path), f"nut_{size}.step")
generate_nut(get_nut(size), path)
return path
def _gen_washer(tmp_path, size):
path = os.path.join(str(tmp_path), f"washer_{size}.step")
generate_washer(get_washer(size), path)
return path
@pytest.mark.parametrize("size", list(BOLT_SIZES))
def test_bolt_across_flats_matches_standard(tmp_path, size):
"""Across-flats S of the hexagon head equals the GOST wrench size."""
path = _gen_bolt(tmp_path, size, 40)
res = validate_model(path)
assert res is not None
assert res.ok, res.failed
@pytest.mark.parametrize("size", list(NUT_SIZES))
def test_nut_across_flats_and_height(tmp_path, size):
"""Nut across-flats S and height k match the GOST table."""
path = _gen_nut(tmp_path, size)
res = validate_model(path)
assert res is not None
assert res.ok, res.failed
@pytest.mark.parametrize("size", list(WASHER_SIZES))
def test_washer_outer_and_thickness(tmp_path, size):
"""Washer outer diameter D and thickness S (k) match the GOST table."""
path = _gen_washer(tmp_path, size)
res = validate_model(path)
assert res is not None
assert res.ok, res.failed
def test_rejects_wrong_flats(tmp_path):
"""Validator must FAIL when the across-flats is outside tolerance."""
res = DimensionResult("bolt_M12_40.step", "bolt", "M12", 0.04, {})
from fastengen.validate_geom import _validate_dim
# Measured 20.0 for a standard S=18 with tol 4% (allowed 17.28..18.72).
_validate_dim(res, "across_flats_S", 18.0, 18.0 * 0.96, 18.0 * 1.04)
assert not res.ok
assert any("across_flats_S" in f for f in res.failed)
def test_dimension_result_ok_when_within_tol(tmp_path):
"""A measurement within tolerance passes."""
res = DimensionResult("nut_M12.step", "nut", "M12", 0.04, {"across_flats_S": 19.0})
from fastengen.validate_geom import _validate_dim
_validate_dim(res, "across_flats_S", 19.0, 19.0 * 0.96, 19.0 * 1.04)
assert res.ok
assert res.failed == []
def test_bbox_min_is_across_flats_for_hexagon(tmp_path):
"""Sanity: hexagon bbox min extent is across-flats (smaller than corners)."""
path = _gen_nut(tmp_path, "M12")
parts = cq.importers.importStep(path)
bb = next(iter(parts.vals())).BoundingBox()
x = bb.xmax - bb.xmin
y = bb.ymax - bb.ymin
assert min(x, y) == pytest.approx(19.0, abs=0.1) # across-flats S
# across-corners of a hexagon = 2 * across-flats / sqrt(3) ~= 1.1547*S
assert max(x, y) == pytest.approx(19.0 * 2 / (3 ** 0.5), abs=0.3)