Skip to content

Commit a31d5eb

Browse files
TMHSDigitalclaude
andauthored
fix(tests): cover the helpers the drift check never saw (#440)
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 <noreply@anthropic.com>
1 parent 9472f4c commit a31d5eb

1 file changed

Lines changed: 59 additions & 4 deletions

File tree

‎tests/check_helper_drift.py‎

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
This hashes each top-level function (docstrings and comments ignored) across
1010
showcase/*/*.py and, for every helper named in CANONICAL, lists the variants
1111
and which pieces carry each. A helper may have at most its declared number of
12-
variants; anything beyond that is drift.
12+
variants; anything beyond that is drift. A helper marked `literals` is hashed
13+
with string constants and ALL_CAPS names blanked, so a per-piece image name or
14+
material index does not count as a new body.
15+
16+
Any function that calls bmesh.ops.convex_hull must be listed here, so a copy
17+
renamed out of CANONICAL is reported instead of drifting unseen (#361).
1318
1419
python tests/check_helper_drift.py report; exit 0 (warn-only)
1520
python tests/check_helper_drift.py --strict exit 1 when a helper drifts
@@ -38,26 +43,73 @@
3843
"hygiene_audit": (7, "per-piece audits report extra keys (euler, nv, edge90)"),
3944
"pack_uvs": (13, "island margins tuned per piece's texel density"),
4045
"assign_slots": (20, "material slot layout is per-piece by nature"),
46+
# #361: helpers that were copied under names the check never saw.
47+
"hull_collider": (15, "each piece's hull is bespoke: material-filtered points "
48+
"(apothecary-shelf), compound per-ring hulls (toboggan); all "
49+
"start from bare points, so the #386 edge fix does not apply"),
50+
"setup_bake_image": (3, "one body, plus the shelf/brazier family that drops the "
51+
"BAKE_RES default and the no-UV early return, plus rope-bridge",
52+
"literals"),
53+
"bake_normal": (4, "one body; deselect_all() instead of the inline loop (11 pieces), "
54+
"a None guard (bowling-pins), one different bake() argument (grain-sacks)"),
55+
"face_area": (5, "equivalent fan-triangulation area, written five ways; some drop "
56+
"the <3-vertex guard, which polygons never need"),
57+
"uv_stats": (7, "pairwise AABB overlap, plus a sorted sweep and a bucketed grid for "
58+
"high-island pieces; the grid variant returns (..., 0.0, 0) on no UVs"),
59+
"zfight_pairs": (9, "two algorithms: shell-aware coplanar test (from grindstone) and "
60+
"a centre-distance test that skips faces sharing a vertex"),
61+
"wire_normal": (10, "the baked normal goes under a bump node where the piece has one, "
62+
"and each piece finds that node its own way", "literals"),
4163
}
4264

65+
# Functions allowed to call bmesh.ops.convex_hull. Every one must be in CANONICAL.
66+
HULL_CALL = ("bmesh", "ops", "convex_hull")
67+
68+
69+
class _BlankLiterals(ast.NodeTransformer):
70+
def visit_Constant(self, n):
71+
return ast.Constant("") if isinstance(n.value, str) else n
4372

44-
def _normalised(fn: ast.FunctionDef) -> str:
73+
def visit_Name(self, n):
74+
return ast.Name("K", n.ctx) if n.id.isupper() else n
75+
76+
77+
def _normalised(fn: ast.FunctionDef, literals: bool = False) -> str:
4578
node = ast.parse(ast.unparse(fn)).body[0]
4679
body = node.body
4780
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) \
4881
and isinstance(body[0].value.value, str):
4982
node.body = body[1:] or [ast.Pass()]
83+
if literals:
84+
node = _BlankLiterals().visit(node)
5085
return ast.dump(node)
5186

5287

88+
def _calls_hull(fn: ast.FunctionDef) -> bool:
89+
return any(isinstance(x, ast.Attribute) and x.attr == HULL_CALL[2]
90+
and ast.unparse(x.value) == ".".join(HULL_CALL[:2]) for x in ast.walk(fn))
91+
92+
93+
def unlisted_hull_callers() -> list[str]:
94+
"""`piece:function` for every bmesh.ops.convex_hull caller not in CANONICAL."""
95+
out = []
96+
for script in sorted(ROOT.glob("showcase/*/*.py")):
97+
for node in ast.parse(script.read_text(encoding="utf-8")).body:
98+
if isinstance(node, ast.FunctionDef) and node.name not in CANONICAL \
99+
and _calls_hull(node):
100+
out.append(f"{script.parent.name}:{node.name}")
101+
return out
102+
103+
53104
def variants() -> dict[str, dict[str, list[tuple[str, str]]]]:
54105
"""name -> hash -> [(piece, source)]"""
55106
out: dict = collections.defaultdict(lambda: collections.defaultdict(list))
56107
for script in sorted(ROOT.glob("showcase/*/*.py")):
57108
tree = ast.parse(script.read_text(encoding="utf-8"))
58109
for node in tree.body:
59110
if isinstance(node, ast.FunctionDef) and node.name in CANONICAL:
60-
h = hashlib.sha1(_normalised(node).encode()).hexdigest()[:8]
111+
literals = "literals" in CANONICAL[node.name][2:]
112+
h = hashlib.sha1(_normalised(node, literals).encode()).hexdigest()[:8]
61113
out[node.name][h].append((script.parent.name, ast.unparse(node)))
62114
return out
63115

@@ -69,7 +121,7 @@ def main(argv=None) -> int:
69121
a = ap.parse_args(argv)
70122
found = variants()
71123
drift = False
72-
for name, (allowed, why) in CANONICAL.items():
124+
for name, (allowed, why, *_flags) in CANONICAL.items():
73125
groups = sorted(found.get(name, {}).items(), key=lambda kv: -len(kv[1]))
74126
n = len(groups)
75127
over = n > allowed
@@ -82,6 +134,9 @@ def main(argv=None) -> int:
82134
print(f" {h} x{len(pieces)}: {names}")
83135
if a.show == name:
84136
print("\n".join(" " + l for l in pieces[0][1].splitlines()))
137+
for caller in unlisted_hull_callers():
138+
drift = True
139+
print(f"DRIFT {caller} calls bmesh.ops.convex_hull but is not in CANONICAL")
85140
if drift:
86141
msg = "helper drift: copies of a shared helper disagree (see above)"
87142
if a.strict:

0 commit comments

Comments
 (0)