Files
civicus-build c7e32d8e07 Seed repository: rev-8.0.0 reference, frozen oracle, toolchain, test harness
Reference implementation of the strap-beam generators at revision 8.0.0, kept
so the acceptance oracle can be regenerated. Not a live target; the running
application has no OpenSCAD dependency.

The oracle holds 123 frozen cases, 113 accepted and 10 rejected, produced by
OpenSCAD 2021.01 with BOSL2 at 92d697c2. The ten rejections are part of the
contract: a port that accepts them is wrong.

tests/test_oracle.py specifies the port API and was written before the port,
so the interface follows from what must be verified rather than what is
convenient to implement. Proven by adversarial stub: a build() that rejects
everything passes all 10 rejection tests and fails all 226 acceptance tests.
2026-08-18 07:30:17 -05:00

154 lines
4.8 KiB
Python

"""
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])