From d4f4a5f50f0c7ce1c60b7f34464303e4bc97c704 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:06:54 -0400 Subject: [PATCH] fix(tests): cover the helpers the drift check never saw check_helper_drift.py only knew five helper names, so copies kept under other names drifted unseen: 15 hull_collider bodies, 75 setup_bake_image bodies, and the uv_stats, face_area, bake_normal, zfight_pairs and wire_normal families. - List those seven helpers in CANONICAL with measured variant counts and the reason each extra variant exists. - A helper marked "literals" is hashed with string constants and ALL_CAPS names blanked, so a per-piece image name or material index is not a new body. setup_bake_image goes from 75 variants to 3 and wire_normal from 11 to 10, without editing 76 scripts. - Any function calling bmesh.ops.convex_hull must be listed, so a hull copied under a new name is reported. The hull_collider copies hull bare points, so the source-edge bug fixed in #386 does not apply to them, and their single delete call over a deduplicated vertex list needs no is_valid guard. Closes #361 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- tests/check_helper_drift.py | 63 ++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/tests/check_helper_drift.py b/tests/check_helper_drift.py index 68355cd0..1ed2bdd3 100644 --- a/tests/check_helper_drift.py +++ b/tests/check_helper_drift.py @@ -9,7 +9,12 @@ This hashes each top-level function (docstrings and comments ignored) across showcase/*/*.py and, for every helper named in CANONICAL, lists the variants and which pieces carry each. A helper may have at most its declared number of -variants; anything beyond that is drift. +variants; anything beyond that is drift. A helper marked `literals` is hashed +with string constants and ALL_CAPS names blanked, so a per-piece image name or +material index does not count as a new body. + +Any function that calls bmesh.ops.convex_hull must be listed here, so a copy +renamed out of CANONICAL is reported instead of drifting unseen (#361). python tests/check_helper_drift.py report; exit 0 (warn-only) python tests/check_helper_drift.py --strict exit 1 when a helper drifts @@ -38,18 +43,64 @@ "hygiene_audit": (7, "per-piece audits report extra keys (euler, nv, edge90)"), "pack_uvs": (13, "island margins tuned per piece's texel density"), "assign_slots": (20, "material slot layout is per-piece by nature"), + # #361: helpers that were copied under names the check never saw. + "hull_collider": (15, "each piece's hull is bespoke: material-filtered points " + "(apothecary-shelf), compound per-ring hulls (toboggan); all " + "start from bare points, so the #386 edge fix does not apply"), + "setup_bake_image": (3, "one body, plus the shelf/brazier family that drops the " + "BAKE_RES default and the no-UV early return, plus rope-bridge", + "literals"), + "bake_normal": (4, "one body; deselect_all() instead of the inline loop (11 pieces), " + "a None guard (bowling-pins), one different bake() argument (grain-sacks)"), + "face_area": (5, "equivalent fan-triangulation area, written five ways; some drop " + "the <3-vertex guard, which polygons never need"), + "uv_stats": (7, "pairwise AABB overlap, plus a sorted sweep and a bucketed grid for " + "high-island pieces; the grid variant returns (..., 0.0, 0) on no UVs"), + "zfight_pairs": (9, "two algorithms: shell-aware coplanar test (from grindstone) and " + "a centre-distance test that skips faces sharing a vertex"), + "wire_normal": (10, "the baked normal goes under a bump node where the piece has one, " + "and each piece finds that node its own way", "literals"), } +# Functions allowed to call bmesh.ops.convex_hull. Every one must be in CANONICAL. +HULL_CALL = ("bmesh", "ops", "convex_hull") + + +class _BlankLiterals(ast.NodeTransformer): + def visit_Constant(self, n): + return ast.Constant("") if isinstance(n.value, str) else n -def _normalised(fn: ast.FunctionDef) -> str: + def visit_Name(self, n): + return ast.Name("K", n.ctx) if n.id.isupper() else n + + +def _normalised(fn: ast.FunctionDef, literals: bool = False) -> str: node = ast.parse(ast.unparse(fn)).body[0] body = node.body if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) \ and isinstance(body[0].value.value, str): node.body = body[1:] or [ast.Pass()] + if literals: + node = _BlankLiterals().visit(node) return ast.dump(node) +def _calls_hull(fn: ast.FunctionDef) -> bool: + return any(isinstance(x, ast.Attribute) and x.attr == HULL_CALL[2] + and ast.unparse(x.value) == ".".join(HULL_CALL[:2]) for x in ast.walk(fn)) + + +def unlisted_hull_callers() -> list[str]: + """`piece:function` for every bmesh.ops.convex_hull caller not in CANONICAL.""" + out = [] + for script in sorted(ROOT.glob("showcase/*/*.py")): + for node in ast.parse(script.read_text(encoding="utf-8")).body: + if isinstance(node, ast.FunctionDef) and node.name not in CANONICAL \ + and _calls_hull(node): + out.append(f"{script.parent.name}:{node.name}") + return out + + def variants() -> dict[str, dict[str, list[tuple[str, str]]]]: """name -> hash -> [(piece, source)]""" out: dict = collections.defaultdict(lambda: collections.defaultdict(list)) @@ -57,7 +108,8 @@ def variants() -> dict[str, dict[str, list[tuple[str, str]]]]: tree = ast.parse(script.read_text(encoding="utf-8")) for node in tree.body: if isinstance(node, ast.FunctionDef) and node.name in CANONICAL: - h = hashlib.sha1(_normalised(node).encode()).hexdigest()[:8] + literals = "literals" in CANONICAL[node.name][2:] + h = hashlib.sha1(_normalised(node, literals).encode()).hexdigest()[:8] out[node.name][h].append((script.parent.name, ast.unparse(node))) return out @@ -69,7 +121,7 @@ def main(argv=None) -> int: a = ap.parse_args(argv) found = variants() drift = False - for name, (allowed, why) in CANONICAL.items(): + for name, (allowed, why, *_flags) in CANONICAL.items(): groups = sorted(found.get(name, {}).items(), key=lambda kv: -len(kv[1])) n = len(groups) over = n > allowed @@ -82,6 +134,9 @@ def main(argv=None) -> int: print(f" {h} x{len(pieces)}: {names}") if a.show == name: print("\n".join(" " + l for l in pieces[0][1].splitlines())) + for caller in unlisted_hull_callers(): + drift = True + print(f"DRIFT {caller} calls bmesh.ops.convex_hull but is not in CANONICAL") if drift: msg = "helper drift: copies of a shared helper disagree (see above)" if a.strict: