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.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
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])
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Acceptance of any reimplementation against the frozen rev-8.0.0 oracle.
|
||||
|
||||
This file is the specification of the port's public API. It was written before
|
||||
the port existed, deliberately: the shape of the interface should be decided by
|
||||
what has to be verified, not by what happens to be convenient to implement.
|
||||
|
||||
Until `mechcomp.profiles.build` exists, everything except the integrity checks
|
||||
skips with a clear reason. Those integrity checks always run -- an oracle that
|
||||
has been edited is worse than no oracle, and that should fail loudly on any
|
||||
machine, at any time, with no dependencies.
|
||||
|
||||
pytest -n auto # all of it
|
||||
pytest -m oracle # acceptance only
|
||||
pytest -k integrity # oracle checks alone, always runnable
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.oracle
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integrity - no dependency on the port
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_integrity_hash(oracle_raw):
|
||||
"""The committed oracle has not been modified."""
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
doc = dict(oracle_raw)
|
||||
recorded = doc.pop("fixtures_sha256")
|
||||
actual = hashlib.sha256(json.dumps(doc, indent=2, sort_keys=True).encode()).hexdigest()
|
||||
assert actual == recorded, (
|
||||
"The oracle has been edited. Its hash covers the document without the "
|
||||
"hash field, serialised with indent=2 and sort_keys=True. If this was "
|
||||
"intentional, regenerate it inside the reference toolchain image and "
|
||||
"record why in FAILURES.md."
|
||||
)
|
||||
|
||||
|
||||
def test_integrity_shape(oracle_raw):
|
||||
"""The case matrix is the one the documents describe."""
|
||||
assert oracle_raw["generator_revision"] == "8.0.0"
|
||||
assert oracle_raw["toolchain"]["openscad"] == "2021.01"
|
||||
assert oracle_raw["toolchain"]["bosl2_commit"].startswith("92d697c2")
|
||||
|
||||
summary = oracle_raw["summary"]
|
||||
assert summary["cases"] == 123
|
||||
assert summary["accepted"] == 113
|
||||
assert summary["rejected"] == 10
|
||||
assert len(oracle_raw["cases"]) == summary["cases"]
|
||||
|
||||
|
||||
def test_integrity_invariants_hold_in_the_oracle(accepted_cases):
|
||||
"""
|
||||
Every accepted case in the oracle satisfies the invariants.
|
||||
|
||||
This validates the oracle itself rather than the port. If it ever fails,
|
||||
the fixture set is describing geometry that should never have been
|
||||
accepted, and no port should be measured against it.
|
||||
"""
|
||||
for case in accepted_cases:
|
||||
r = case["report"]
|
||||
where = f"{case['profile']}/{case['label']}"
|
||||
expected_members = 3 if "3x" in case["generator"] else 4
|
||||
|
||||
assert r["SECTION_PARTS"] == 1, f"{where}: not one connected solid"
|
||||
assert r["STRAP_CHANNELS"] == expected_members, f"{where}: channels merged"
|
||||
assert r["MIN_WALL_ACTUAL_MM"] >= r["MIN_WALL_SPEC_MM"] - 1e-4, \
|
||||
f"{where}: wall below its declared minimum"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Acceptance - requires the port
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_accepted_case_matches_oracle(port, accepted_case, tolerance):
|
||||
"""
|
||||
A case the reference accepted must be accepted, with matching geometry.
|
||||
|
||||
Both halves matter. Reproducing the measured values while accepting a case
|
||||
the reference rejected is not a passing port.
|
||||
"""
|
||||
expected = accepted_case["report"]
|
||||
where = f"{accepted_case['profile']}/{accepted_case['label']}"
|
||||
|
||||
result = port.build(
|
||||
family=accepted_case["family"],
|
||||
profile=accepted_case["profile"],
|
||||
params=accepted_case["params"],
|
||||
)
|
||||
got = result.report
|
||||
|
||||
# Counts are exact.
|
||||
for key in ("SECTION_PARTS", "STRAP_CHANNELS", "BUNDLE_COUNT"):
|
||||
assert got[key] == expected[key], f"{where}: {key}"
|
||||
|
||||
# Lengths and areas carry the tolerance the oracle declares.
|
||||
for key, want in expected.items():
|
||||
if not isinstance(want, float) or key in ("SECTION_PARTS", "STRAP_CHANNELS"):
|
||||
continue
|
||||
tol = tolerance["areas_mm2"] if key.endswith("_MM2") else tolerance["lengths_mm"]
|
||||
assert key in got, f"{where}: port did not report {key}"
|
||||
assert abs(got[key] - want) <= tol, (
|
||||
f"{where}: {key} is {got[key]}, oracle says {want}"
|
||||
)
|
||||
|
||||
|
||||
def test_rejected_case_is_rejected(port, rejected_case):
|
||||
"""
|
||||
A case the reference rejected must be rejected.
|
||||
|
||||
These ten are the part of the contract a naive reimplementation loses: it
|
||||
is easy to reproduce the geometry and quietly drop the constraint that made
|
||||
it trustworthy. Accepting any of them is a failure, however good the
|
||||
numbers look elsewhere.
|
||||
"""
|
||||
where = f"{rejected_case['profile']}/{rejected_case['label']}"
|
||||
with pytest.raises(port.ProfileRejected) as excinfo:
|
||||
port.build(
|
||||
family=rejected_case["family"],
|
||||
profile=rejected_case["profile"],
|
||||
params=rejected_case["params"],
|
||||
)
|
||||
assert str(excinfo.value).strip(), (
|
||||
f"{where}: rejected without a message. A rejection must name the "
|
||||
f"parameter and the limit, as the reference does:\n"
|
||||
f" {rejected_case['rejection']}"
|
||||
)
|
||||
|
||||
|
||||
def test_no_cad_dependency_on_the_2d_path(port, accepted_case):
|
||||
"""
|
||||
Building a cross-section must not import the 3D kernel.
|
||||
|
||||
ENVIRONMENT.md section 1.1: a slow OCCT import must never land in the
|
||||
request path for a page that only draws a cross-section. This test also
|
||||
runs in the CI job where requirements-cad.txt is absent, where an accidental
|
||||
import fails outright rather than merely being slow.
|
||||
"""
|
||||
import sys
|
||||
|
||||
for module in ("cadquery", "OCP", "build123d"):
|
||||
sys.modules.pop(module, None)
|
||||
|
||||
port.build(
|
||||
family=accepted_case["family"],
|
||||
profile=accepted_case["profile"],
|
||||
params=accepted_case["params"],
|
||||
)
|
||||
|
||||
leaked = [m for m in ("cadquery", "OCP", "build123d") if m in sys.modules]
|
||||
assert not leaked, f"the 2D path imported {leaked}"
|
||||
Reference in New Issue
Block a user