#!/usr/bin/env python3 """Adversarial tests for verify-ridgeline.py — both polarities. Per ADR 0005, a geometric contract is a checker plus fixtures proving it fires when it should and stays quiet when it shouldn't. Each mutation below renders perfectly and no other gate would catch it: one ridge drawn on its own amplitude, a baseline nudged off the pitch to buy headroom, a spline through binned counts, a printed range wider than the mass it claims, a name label sitting on a neighbour's row. The synthetic figures at the end prove the checker reads the FIGURE's own amplitude and pitch rather than memorising the shipped layout: a valid ridgeline at a different top, pitch and scale must pass, and the budget rules must fire at BOTH ends on counts the shipped example cannot be mutated into. The last case pins the scope treaty with the sibling Line-variant checkers. `data-bins` on a `` is this contract's vocabulary; `data-series`, `data-ranks` and `data-layer` belong to the slopegraph, bump and streamgraph gates. Every sibling checker present in the tree is handed the ridgeline examples directly and must skip them without a word — a sibling that claimed one would reject it for lacking elements it never said it had. The reverse direction is asserted too: this checker must skip the siblings' shipped examples. Usage: python3 scripts/test-verify-ridgeline.py Exit: 0 all pass, 1 a case failed. """ from __future__ import annotations import os import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent CHECKER = ROOT / "scripts/verify-ridgeline.py" GOOD = ROOT / "skills/diagram-design/assets/example-ridgeline.html" # The sibling Line-variant gates. Any that exists in the tree must skip the # ridgeline examples rather than claim them; the ones not yet landed are absent, # and their absence is printed rather than passed over in silence. SIBLINGS = ["verify-slopegraph.py", "verify-bump.py", "verify-streamgraph.py"] RIDGELINES = sorted((ROOT / "skills/diagram-design/assets").glob("example-ridgeline*.html")) CHILD_ENV = {**os.environ, "PYTHONIOENCODING": "utf-8"} # Anchors, all literal excerpts of the shipped example. A fixture that fails to # build means the example moved, which is itself worth knowing. FOCAL_D = ('d="M320,320 L350,317.6 L380,305.6 L410,279.2 L440,269.6 L470,286.4 ' 'L500,300.8 L530,305.6 L560,303.2 L590,298.4 L620,303.2 L650,310.4 L680,320 Z"') FOCAL_PATH = ('') # The same ridge redrawn at 3.6px per unit instead of the figure's 2.4. Every # vertex still sits on a straight, closed outline over the same bins; only the # scale is private. This is the defect the type exists to prevent. PRIVATE_AMPLITUDE_D = ('d="M320,320 L350,316.4 L380,298.4 L410,258.8 L440,244.4 ' 'L470,269.6 L500,291.2 L530,298.4 L560,294.8 L590,287.6 ' 'L620,294.8 L650,305.6 L680,320 Z"') REPORT_RULE = ('') def invoke(argv): result = subprocess.run( argv, capture_output=True, text=True, encoding="utf-8", errors="replace", env=CHILD_ENV, ) return result.returncode, (result.stdout or "") + (result.stderr or "") def run(*paths): return invoke([sys.executable, str(CHECKER), *(str(p) for p in paths)]) def write(directory, name, source): path = directory / name path.write_text(source, encoding="utf-8", newline="\n") return path def case(failures, directory, name, source, original, expect, describe): """One mutation: it must be a real edit, and it must be rejected.""" if source == original: failures.append("could not build the %s fixture (anchor moved)" % name) return code, output = run(write(directory, "%s.html" % name, source)) if code == 0: failures.append("%s was accepted" % describe) elif expect not in output: failures.append("%s was reported without the expected reason: %s" % (describe, output.strip())) else: print("OK: %s is rejected" % describe) def hump(n_bins, peak_at, height=12, step=3): """A closed distribution: zero at both ends, one interior peak.""" values = [] for j in range(n_bins): if j in (0, n_bins - 1): values.append(0) else: values.append(max(0, height - step * abs(j - peak_at))) return values def synthetic(n_ridges, n_bins, top=100, pitch=40, amp=2.0, x0=200, dx=20, unit=10): """A minimal valid ridgeline: n_ridges over n_bins, on its own grid.""" xs = [x0 + i * dx for i in range(n_bins)] parts = [ 'synthetic ridgeline', '', 'synthetic ridgeline', 'ridgeline fixture', ] for index, (x, value) in enumerate(((xs[0], 0), (xs[-1], (n_bins - 1) * unit))): parts.append('%d' % (index, value, x, value)) for s in range(n_ridges): baseline = top + s * pitch values = hump(n_bins, 1 + (s % max(1, n_bins - 2))) pts = ["%g,%g" % (x, baseline - amp * v) for x, v in zip(xs, values)] d = "M" + pts[0] + " " + " ".join("L" + p for p in pts[1:]) + " Z" focal = s == 0 stroke = "#eb6c36" if focal else "rgba(45,49,66,0.70)" width = "2.4" if focal else "1.2" live = [i for i, v in enumerate(values) if v > 0] parts.append('' % (s, xs[0], baseline, xs[-1], baseline)) parts.append('' % (s, baseline, ",".join(str(v) for v in values), d, stroke, width)) parts.append('r%d' % (s, xs[0] - 16, baseline + 3.5, s)) parts.append('%d–%d ms' % (s, xs[-1] + 16, baseline + 3.5, live[0] * unit, live[-1] * unit)) parts.append("") return "\n".join(parts) def main() -> int: source = GOOD.read_text(encoding="utf-8") failures = [] with tempfile.TemporaryDirectory() as raw: directory = Path(raw) # 1. The shipped example must pass untouched. code, output = run(GOOD) if code != 0: failures.append("clean example was rejected: %s" % output.strip()) else: print("OK: the shipped ridgeline passes") # 2. THE defect: one ridge redrawn on its own amplitude. Every other # check stays green — the outline is straight, closed, on the shared # bins, and its labels are correct. case( failures, directory, "private-amplitude", source.replace(FOCAL_D, PRIVATE_AMPLITUDE_D, 1), source, "one amplitude", "a ridge drawn on its own amplitude", ) # 3. A baseline nudged off the pitch to buy one ridge headroom, with its # rule, its outline and its labels all moved to match. case( failures, directory, "off-pitch", source.replace('data-baseline="320"', 'data-baseline="328"', 1) .replace(FOCAL_D, FOCAL_D.replace(",320 ", ",328 ") .replace(",320\"", ",328\""), 1) .replace('checkout-api', 'text-anchor="end">checkout', 1), source, "binding must agree", "a name label reading a different name than it binds", ) case( failures, directory, "unlabelled", source.replace(' 40–440 ms\n', "", 1), source, "prints no range label", "a ridge with no range label", ) # 12. A printed range wider than the mass it claims. Every geometric # check still passes; only the sentence beside the ridge is false. case( failures, directory, "false-range", source.replace(">40–440 ms<", ">40–480 ms<", 1), source, "on the figure's own tick scale", "a printed range wider than the ridge's nonzero bins", ) # 13. Bin ticks: text edited without its binding, and a tick moved off # the bins it labels. case( failures, directory, "tick-swap", source.replace('text-anchor="middle">240', 'text-anchor="middle">280', 1), source, "must state one number", "a tick whose visible text was edited without its binding", ) case( failures, directory, "tick-off-bins", source.replace('data-tick="2" data-bin="240" x="500"', 'data-tick="2" data-bin="240" x="505"', 1), source, "not a bin position", "a tick drawn off the bins it labels", ) # 14. Transforms: on the ridge, on an ancestor, and from CSS. Raw # coordinates are the basis, so any of the three moves the rendered # mark away from the bin it was checked against. case( failures, directory, "transformed", source.replace('' + FOCAL_PATH + "", 1), source, "ancestor", "a ridge wrapped in a transformed group", ) case( failures, directory, "css-transform", source.replace("