web: the composer, served behind the existing proxy chain

A browser page with controls on one side and the cross section on the other. Three separately classed layers: printed material, the cavities the stock passes through, and the stock drawn inside its cavity so the fit gap is visible. Under it the report and the design record, so the identity of a part is visible while tuning rather than discovered afterwards. Standard library only, no framework, no build step, no new dependency.

Binding comes from /etc/mechcomp/mechcomp.env, which already declared 10.20.0.10:8770. An earlier draft invented a port on 0.0.0.0, which would have placed a second unproxied plaintext copy beside the proxied one. The fallback with no env file is loopback, never every interface: behind a proxy, binding too narrowly fails loudly as a 502 and too widely fails silently as an open service.

Controls are filtered to the selected profile, with no catch all group. A knob that moves nothing is worse than an absent one.

Renderer tests assert what an eye cannot: the vertical flip happens exactly once, holes render as holes, and the cavity is larger than the stock inside it. Seven mutations on the renderer, all caught.
This commit is contained in:
2026-09-11 06:46:23 -05:00
parent e921cacf0e
commit 14514b0bac
5 changed files with 874 additions and 1 deletions
+318
View File
@@ -0,0 +1,318 @@
"""
The renderer and the composer, tested the way a viewer can actually fail.
A picture that looks plausible while being wrong is worse than no picture, and
this is the first part of the system CIVICVS will judge by eye rather than by
test output. So the things asserted here are the ones an eye cannot check: that
the flip is applied exactly once, that holes are not filled in, that a cavity
really is larger than the stock inside it, and that a rejection reaches the
screen as a message rather than a stack trace.
"""
from __future__ import annotations
import re
import pytest
from mechcomp import svg
SQUARE = [[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)]]
WITH_HOLE = [
[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)],
[(4.0, 4.0), (6.0, 4.0), (6.0, 6.0), (4.0, 6.0)],
]
# ---------------------------------------------------------------------------
# The renderer
# ---------------------------------------------------------------------------
def test_every_ring_reaches_the_path():
"""A hole silently dropped would render as solid material."""
d = svg.path_d(WITH_HOLE)
assert d.count("M ") == 2, d
assert d.count("Z") == 2
assert "4 4" in d.replace(".0", "")
def test_holes_use_evenodd_so_winding_does_not_matter():
"""
Ring winding is not something this module should have to know. evenodd
renders a hole as a hole either way, and its absence is the difference
between a bore and a solid plug.
"""
out = svg.render(WITH_HOLE)
assert out.count('fill-rule="evenodd"') >= 1
def test_degenerate_rings_are_skipped_not_emitted():
assert svg.path_d([[(0.0, 0.0), (1.0, 1.0)]]) == ""
assert svg.path_d([[]]) == ""
def test_the_vertical_flip_is_applied_exactly_once():
"""
Geometry has +Y up, SVG has +Y down. Flipping twice, or not at all, gives a
part that is upside down -- which for an asymmetric profile like T or A
Frame is wrong in a way that is easy to miss and impossible to print
around.
"""
out = svg.render(SQUARE)
assert out.count("scale(1,-1)") == 1
assert len(re.findall(r"transform=", out)) == 1
def test_coordinates_are_left_in_geometry_space():
"""
The flip is in the transform, so the numbers in the file are the geometry's
own and can be read against the report. Negating them instead would make
the file unreadable against anything.
"""
out = svg.render(SQUARE)
assert " 10 " in out or " 10 10" in out
assert "-10" not in svg.path_d(SQUARE)
def test_viewbox_contains_the_geometry_plus_margin():
out = svg.render(SQUARE, margin=3.0)
vb = re.search(r'viewBox="([-\d. ]+)"', out).group(1).split()
x0, y0, w, h = (float(v) for v in vb)
assert x0 == pytest.approx(-3.0)
assert y0 == pytest.approx(-3.0)
assert w == pytest.approx(16.0)
assert h == pytest.approx(16.0)
def test_layers_are_separately_classed():
"""
ROADMAP section 4's stated reason for the port. A flattened silhouette
would not have justified leaving OpenSCAD.
"""
out = svg.render(SQUARE, cavities=WITH_HOLE, stock=[SQUARE[0]])
for cls in ("mc-layer-material", "mc-layer-cavity", "mc-layer-stock"):
assert cls in out, cls
def test_optional_layers_are_omitted_when_absent():
out = svg.render(SQUARE)
assert "mc-layer-material" in out
assert "mc-layer-cavity" not in out
assert "mc-layer-stock" not in out
def test_bounds_of_nothing_does_not_divide_by_zero():
assert svg.bounds([]) == (-1.0, -1.0, 1.0, 1.0)
assert svg.render([]).startswith("<svg")
# ---------------------------------------------------------------------------
# Against real geometry
# ---------------------------------------------------------------------------
def test_a_real_profile_renders_with_all_three_layers():
build = pytest.importorskip("mechcomp.profiles").build
result = build(family="3x", profile="Y", params={})
out = svg.render_result(result)
assert out.startswith("<svg") and out.rstrip().endswith("</svg>")
for cls in ("mc-layer-material", "mc-layer-cavity", "mc-layer-stock"):
assert cls in out, "%s missing from a real render" % cls
assert out.count("scale(1,-1)") == 1
def test_the_cavity_is_drawn_larger_than_the_stock_in_it():
"""
The fit gap is the reason to look at the picture at all. If cavity and stock
render identically, the clearance is invisible and the viewer is useless for
the one judgement it exists to support.
"""
build = pytest.importorskip("mechcomp.profiles").build
from mechcomp.geom.join import cavity_region
from mechcomp.geom.records import strap_path
from mechcomp.geom.region import area as region_area
result = build(family="3x", profile="Y", params={"fit_clearance_mm": 0.4})
cav = cavity_region(result.members, result.geo)
stock = [strap_path(m, result.geo) for m in result.members]
assert region_area(cav) > region_area(stock), (
"cavity is not larger than the stock it holds")
def test_every_profile_in_both_families_renders():
"""A viewer that works for Y and breaks on Cross is not a catalogue."""
profiles = pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import families
for name, family in families().items():
for profile in sorted(family.catalogue):
try:
result = profiles.build(family=name, profile=profile, params={})
except profiles.ProfileRejected:
continue
out = svg.render_result(result)
assert out.startswith("<svg"), "%s/%s" % (name, profile)
assert "mc-layer-material" in out, "%s/%s" % (name, profile)
# ---------------------------------------------------------------------------
# The composer payload
# ---------------------------------------------------------------------------
def test_binding_comes_from_the_deployment_not_from_a_guess(tmp_path,
monkeypatch):
"""
The deployment already declares where this service lives. Inventing a
port put it somewhere nginx does not look and, worse, on every interface
the container has.
"""
from mechcomp.web.app import base_url, resolve_binding
for key in ("MECHCOMP_BIND", "MECHCOMP_PORT", "MECHCOMP_BASE_URL"):
monkeypatch.delenv(key, raising=False)
envf = tmp_path / "mechcomp.env"
envf.write_text(
"# staging\n"
"MECHCOMP_ENV=staging\n"
"MECHCOMP_BIND=10.20.0.10 # scalar; loopback is NOT bound\n"
"MECHCOMP_PORT=8770\n"
"MECHCOMP_BASE_URL=https://mechanical-compiler.dev.infra\n")
assert resolve_binding(str(envf)) == ("10.20.0.10", 8770)
assert base_url(str(envf)) == "https://mechanical-compiler.dev.infra"
def test_a_real_environment_variable_wins_over_the_file(tmp_path, monkeypatch):
from mechcomp.web.app import resolve_binding
envf = tmp_path / "mechcomp.env"
envf.write_text("MECHCOMP_BIND=10.20.0.10\nMECHCOMP_PORT=8770\n")
monkeypatch.setenv("MECHCOMP_PORT", "9001")
monkeypatch.delenv("MECHCOMP_BIND", raising=False)
assert resolve_binding(str(envf)) == ("10.20.0.10", 9001)
def test_the_fallback_is_loopback_never_every_interface(tmp_path, monkeypatch):
"""
Behind a reverse proxy, binding too narrowly fails loudly as a 502 and
binding too widely fails silently as an open service. If this ever
returns 0.0.0.0 by default, the quiet failure is back.
"""
from mechcomp.web.app import resolve_binding
for key in ("MECHCOMP_BIND", "MECHCOMP_PORT"):
monkeypatch.delenv(key, raising=False)
host, port = resolve_binding(str(tmp_path / "absent.env"))
assert host == "127.0.0.1", host
assert host not in ("0.0.0.0", "::", ""), "bound every interface"
assert port == 8770
def test_a_malformed_env_file_does_not_widen_the_binding(tmp_path, monkeypatch):
from mechcomp.web.app import resolve_binding
for key in ("MECHCOMP_BIND", "MECHCOMP_PORT"):
monkeypatch.delenv(key, raising=False)
envf = tmp_path / "mechcomp.env"
envf.write_text("garbage\nMECHCOMP_PORT=not-a-number\n[section]\n")
host, port = resolve_binding(str(envf))
assert host == "127.0.0.1"
assert port == 8770
def test_payload_carries_a_picture_a_report_and_an_identity():
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
p = build_payload("3x", "Y", {})
assert p["ok"] is True
assert p["svg"].startswith("<svg")
assert p["report"]["PROFILE"] == "Y"
assert p["input_id"], "no design id on the payload"
assert "MECHCOMP DESIGN RECORD" in p["record"]
def test_changing_a_control_changes_the_part_and_its_identity():
"""The composer's whole premise, end to end."""
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
a = build_payload("3x", "Y", {"fit_clearance_mm": "0.15"})
b = build_payload("3x", "Y", {"fit_clearance_mm": "0.35"})
assert a["ok"] and b["ok"]
assert a["svg"] != b["svg"], "the drawing did not change"
assert a["input_id"] != b["input_id"], "two settings share one design id"
def test_form_strings_are_coerced_to_the_types_the_compiler_expects():
"""Everything arrives from a browser as text; ints must not become floats."""
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
p = build_payload("3x", "Y", {"bundle_count": "2", "fit_clearance_mm": "0.3"})
assert p["ok"] is True
assert p["values"]["bundle_count"] == 2
assert isinstance(p["values"]["bundle_count"], int)
assert p["values"]["fit_clearance_mm"] == 0.3
def test_a_rejection_is_a_message_not_a_crash():
"""
A rejection is the compiler working. It must read as an explanation on the
screen, and it must still name the parameter and the limit.
"""
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
p = build_payload("3x", "Y", {"min_wall_mm": "0"})
assert p["ok"] is False
assert p["message"].strip()
assert "trace" not in p
assert "min_wall_mm" in p["message"]
def test_nonsense_input_does_not_take_the_page_down():
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
p = build_payload("3x", "Y", {"fit_clearance_mm": "banana",
"not_a_parameter": "7"})
assert "ok" in p
assert "not_a_parameter" not in p["values"]
def test_controls_offered_are_relevant_to_the_selected_profile():
"""
Thirty controls of which twenty do nothing is not a composer. Selecting Y
must not present the Three-Fin parameters.
"""
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload
y = build_payload("3x", "Y", {})
offered = {k for _, keys in y["groups"] for k in keys}
assert any(k.startswith("y_") for k in offered)
assert not any(k.startswith("three_fin_") for k in offered), sorted(offered)
assert "strap_width_mm" in offered and "fit_clearance_mm" in offered
def test_every_offered_control_is_a_real_parameter():
"""A control that does nothing is worse than a missing one."""
pytest.importorskip("mechcomp.profiles")
from mechcomp.web.app import build_payload, families
for name, family in families().items():
for profile in sorted(family.catalogue):
p = build_payload(name, profile, {})
for _, keys in p["groups"]:
for key in keys:
assert key in family.defaults, (
"%s/%s offers %s, which is not a parameter"
% (name, profile, key))