""" Shared fixtures. The main job here is translating an oracle case into a call the port can make. The oracle records OpenSCAD command-line overrides, because that is what produced it; the port takes a plain dictionary. That translation lives in one place so the acceptance tests stay readable and the mapping is auditable. The parameter names are deliberately unchanged. `bundle_count` in the OpenSCAD generator is `bundle_count` in the port. A renaming layer would be one more thing to get wrong for no benefit. """ from __future__ import annotations import json from pathlib import Path import pytest ORACLE = ( Path(__file__).resolve().parents[1] / "fixtures" / "strap-beam-8.0.0" / "strap-beam-fixtures-8.0.0.json" ) PORT_MISSING = ( "the Shapely port does not exist yet: mechcomp.profiles.build is not " "importable. This is expected until the first work item in ROADMAP.md is " "done. The integrity tests still run." ) # --------------------------------------------------------------------------- # Oracle loading # --------------------------------------------------------------------------- def _parse_define(token: str) -> tuple[str, object]: """ Turn one OpenSCAD -D override into a (name, value) pair. -Dprofile_type="Y" -> ("profile_type", "Y") -Dbundle_count=2 -> ("bundle_count", 2) -Dstrap_width_mm=13.4-> ("strap_width_mm", 13.4) """ assert token.startswith("-D"), f"not an override: {token!r}" name, _, raw = token[2:].partition("=") raw = raw.strip() if raw.startswith('"') and raw.endswith('"'): return name, raw[1:-1] try: value = float(raw) except ValueError: return name, raw return name, int(value) if value.is_integer() and "." not in raw else value def _case_params(case: dict) -> dict: """Every override except profile_type, which is passed separately.""" params = dict(_parse_define(t) for t in case["defs"]) params.pop("profile_type", None) return params def _enrich(case: dict) -> dict: return { **case, "family": "3x" if "3x" in case["generator"] else "4x", "params": _case_params(case), } @pytest.fixture(scope="session") def oracle_raw() -> dict: if not ORACLE.exists(): pytest.fail(f"oracle missing: {ORACLE}") return json.loads(ORACLE.read_text()) @pytest.fixture(scope="session") def tolerance(oracle_raw) -> dict: return oracle_raw["tolerance"] @pytest.fixture(scope="session") def all_cases(oracle_raw) -> list[dict]: return [_enrich(c) for c in oracle_raw["cases"]] @pytest.fixture(scope="session") def accepted_cases(all_cases) -> list[dict]: return [c for c in all_cases if c["outcome"] == "ok"] @pytest.fixture(scope="session") def rejected_cases(all_cases) -> list[dict]: return [c for c in all_cases if c["outcome"] == "rejected"] # --------------------------------------------------------------------------- # The port under test # --------------------------------------------------------------------------- @pytest.fixture(scope="session") def port(): """ The module implementing the generators. Required surface: build(family: str, profile: str, params: dict) -> Result Result.report -> dict, the SB_* keys the oracle records raises ProfileRejected for an unbuildable arrangement ProfileRejected(Exception) str() must name the parameter and the limit, as the reference does. """ profiles = pytest.importorskip("mechcomp.profiles", reason=PORT_MISSING) if not hasattr(profiles, "build"): pytest.skip(PORT_MISSING) return profiles # --------------------------------------------------------------------------- # Per-case parametrisation # --------------------------------------------------------------------------- def _load_cases_eagerly() -> list[dict]: """ Read the oracle at collection time. Parametrisation happens before fixtures resolve, so this cannot use them. A missing oracle yields an empty list, and the integrity tests report the real problem rather than every acceptance test failing obscurely. """ if not ORACLE.exists(): return [] return [_enrich(c) for c in json.loads(ORACLE.read_text())["cases"]] _CASES = _load_cases_eagerly() def _ident(case: dict) -> str: return f"{case['family']}-{case['profile'].replace(' ', '_')}-{case['label']}" def pytest_generate_tests(metafunc): if "accepted_case" in metafunc.fixturenames: cases = [c for c in _CASES if c["outcome"] == "ok"] metafunc.parametrize("accepted_case", cases, ids=[_ident(c) for c in cases]) if "rejected_case" in metafunc.fixturenames: cases = [c for c in _CASES if c["outcome"] == "rejected"] metafunc.parametrize("rejected_case", cases, ids=[_ident(c) for c in cases])