81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
"""Regression test for a SolidCraft design. One session walks the parameters through the cases in
|
|
order, as edits in the app do, and compares a body after each case with frozen values.
|
|
Copy next to the design, then fill in DESIGN, BODY and CASES. Needs solidcraft-cli on PATH."""
|
|
|
|
import json
|
|
import subprocess
|
|
from math import dist
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from typing import NamedTuple
|
|
|
|
import pytest
|
|
|
|
DESIGN = Path(__file__).parent / "design.json"
|
|
BODY = "Body"
|
|
# SolidCraft measures a tessellation, and its volume and area error depends on the shape. Measure it
|
|
# for this design: run.py's BODY volume against the OCCT volume compare_step.py prints for the
|
|
# design's own STEP export (compared with itself). Set REL to about 3 times that error.
|
|
REL = 2e-4
|
|
|
|
|
|
class Measures(NamedTuple):
|
|
volume: float
|
|
area: float
|
|
center_of_mass: tuple[float, float, float]
|
|
bbox_min: tuple[float, float, float]
|
|
bbox_max: tuple[float, float, float]
|
|
|
|
|
|
# Parameter values of each case, in the order the session applies them, and the body's expected
|
|
# measures: from an exact reference when porting, from hand calculation for simple shapes, or a
|
|
# frozen build the user has reviewed. A case changes only the parameters it names; the others keep
|
|
# the values of the cases before it.
|
|
CASES = {
|
|
# (("rows", "7"), ("cells", "4")): Measures(volume=..., area=..., center_of_mass=(...), bbox_min=(...), bbox_max=(...)),
|
|
}
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def inspections() -> dict:
|
|
"""document.inspect after each case, all in one session. A fixture, so that a failed session
|
|
is reported to every test instead of rerun for each."""
|
|
commands, at, current = [], [], {}
|
|
for case in CASES:
|
|
for name, value in case:
|
|
if current.get(name) != value:
|
|
commands.append({"command": "parameters.change", "params": {"name": name, "expression": value}})
|
|
current[name] = value
|
|
at.append(len(commands))
|
|
commands.append({"command": "document.inspect", "params": {"measure": True}})
|
|
with TemporaryDirectory() as tmp:
|
|
script = Path(tmp) / "script.json"
|
|
script.write_text(json.dumps({"commands": commands}))
|
|
done = subprocess.run(
|
|
["solidcraft-cli", "run", script, "--in", DESIGN, "--results"], capture_output=True, text=True
|
|
)
|
|
assert done.returncode == 0, done.stderr
|
|
results = json.loads(done.stdout)
|
|
return {case: results[index] for case, index in zip(CASES, at)}
|
|
|
|
|
|
def body(inspection: dict) -> Measures:
|
|
found = {b["name"]: b for b in inspection["bodies"]}[BODY]
|
|
bbox = found["bbox"]
|
|
return Measures(found["volume_mm3"], found["area_mm2"], tuple(found["center_of_mass"]),
|
|
tuple(bbox["min"]), tuple(bbox["max"]))
|
|
|
|
|
|
def assert_matches(actual: Measures, expected: Measures) -> None:
|
|
assert actual.volume == pytest.approx(expected.volume, rel=REL)
|
|
assert actual.area == pytest.approx(expected.area, rel=REL)
|
|
# Centre of mass and bounding box come out up to about 5e-5 of the part size off; allow 3 times that
|
|
tolerance = max(1e-3, 1.5e-4 * dist(expected.bbox_min, expected.bbox_max))
|
|
for point in ("center_of_mass", "bbox_min", "bbox_max"):
|
|
assert getattr(actual, point) == pytest.approx(getattr(expected, point), abs=tolerance), point
|
|
|
|
|
|
@pytest.mark.parametrize("case", CASES)
|
|
def test_body_matches(inspections, case):
|
|
assert_matches(body(inspections[case]), CASES[case])
|