Skip to content

Commit 2c88240

Browse files
TMHSDigitalclaude
andcommitted
test: a content-smoke falsifier counts only when the check is what fails
run_content wrapped setup, the edited snippet and the check in one except, so a falsify edit that crashed the snippet (or made the check hit a NameError) was reported FALSIFIED, proving nothing about the check. The example catalog already refuses exit 1 for the same reason. run_snippet now reports which phase raised; protocol.classify_content_falsifier accepts only a check-phase failure that is not a broken-code error, with unit tests. An empty run now fails, and SystemExit is caught instead of aborting the run. Verified on Blender 5.2.1 LTS: all 31 snippet falsifiers still FALSIFIED, each from the check phase; 9 skill blocks pass. Closes #519 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXRvikhJZSVGBFM91Dv Signed-off-by: TM Hospitality Strategies <TMHospitalityStrategies@gmail.com>
1 parent e0e109f commit 2c88240

3 files changed

Lines changed: 77 additions & 14 deletions

File tree

‎tests/smoke/protocol.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,31 @@ def summarize_records(records):
187187
if passed == 0:
188188
return passed, skipped, failed, 2
189189
return passed, skipped, failed, 0
190+
191+
192+
# Content smoke (run_content.py) falsifiers. A falsify edit proves a snippet's
193+
# check works only when the check itself fails. An exception while running the
194+
# setup or the edited snippet means the edit broke the code, not the contract,
195+
# and so do these errors raised during the check: they say the edited code no
196+
# longer resolves, which any edit can cause (#519). The example catalog draws
197+
# the same line by refusing exit 1, the uncaught-exception wrapper.
198+
FALSIFIED = "FALSIFIED"
199+
BROKEN_CODE_ERRORS = frozenset({
200+
"NameError", "UnboundLocalError", "SyntaxError", "IndentationError",
201+
"ImportError", "ModuleNotFoundError",
202+
})
203+
204+
205+
def classify_content_falsifier(phase, error_type):
206+
"""(outcome, reason) for one snippet falsify run.
207+
208+
phase: None when setup, snippet and check all ran clean, else the phase that
209+
raised ("setup", "snippet" or "check"). error_type: the exception's class name.
210+
"""
211+
if phase is None:
212+
return FAIL, "check still passed"
213+
if phase != "check":
214+
return FAIL, f"falsify edit crashed the {phase}, not the check"
215+
if error_type in BROKEN_CODE_ERRORS:
216+
return FAIL, f"check raised {error_type}: the edit broke the code, not the contract"
217+
return FALSIFIED, ""

‎tests/smoke/run_content.py‎

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
99
- Snippets: each entry in tests/smoke/snippet_checks.py runs the snippet, then
1010
its check, in a fresh factory scene (PASS), then reruns it with the entry's
11-
``falsify`` edit applied, which must make the check fail (FALSIFIED).
11+
``falsify`` edit applied, which must make the check fail (FALSIFIED). The
12+
check has to be what fails: the edit crashing the snippet, or the check
13+
raising NameError-style errors, is FAIL (#519).
1214
- Skill blocks: a ```python fence directly under a ``<!-- smoke: run -->``
1315
line is exec'd as-is in a fresh factory scene; it carries its own asserts.
1416
@@ -32,6 +34,7 @@
3234
sys.path.insert(0, HERE)
3335
sys.dont_write_bytecode = True
3436
from snippet_checks import CASES # noqa: E402
37+
from protocol import classify_content_falsifier # noqa: E402
3538

3639
TAG = re.compile(r"^<!-- smoke: run -->[ \t]*\r?\n```python[ \t]*\r?\n(.*?)\r?\n```", re.M | re.S)
3740
SCRATCH = tempfile.mkdtemp(prefix="bdt-content-")
@@ -58,28 +61,37 @@ def fresh_scene():
5861

5962

6063
def run_snippet(name, source, case):
61-
"""Exec the snippet then its check; return None on pass, else the error line."""
64+
"""Exec the snippet then its check.
65+
66+
Returns None on pass, else (phase, error type name, error line), where phase
67+
is the step that raised: "setup", "snippet" or "check". A falsifier only
68+
counts when the check is what failed (protocol.classify_content_falsifier).
69+
"""
6270
fresh_scene()
6371
path = os.path.join(REPO, "snippets", name)
6472
ns = {"__name__": "snippet_" + name[:-3].replace("-", "_"), "__file__": path,
6573
"__package__": "bl_ext.user_default.bdt_content_smoke",
6674
"bpy": bpy, "np": np, "tmp": tmp, "glb_json": glb_json}
67-
try:
68-
if case.get("setup"):
69-
exec(compile(case["setup"], f"{name}:setup", "exec"), ns)
70-
exec(compile(source, path, "exec"), ns)
71-
exec(compile(case["check"], f"{name}:check", "exec"), ns)
72-
except Exception as e: # noqa: BLE001 -- any failure is the result
73-
tb = traceback.extract_tb(e.__traceback__)[-1]
74-
return f"{type(e).__name__}: {e} ({os.path.basename(tb.filename)}:{tb.lineno})"
75+
steps = []
76+
if case.get("setup"):
77+
steps.append(("setup", case["setup"], f"{name}:setup"))
78+
steps.append(("snippet", source, path))
79+
steps.append(("check", case["check"], f"{name}:check"))
80+
for phase, code, label in steps:
81+
try:
82+
exec(compile(code, label, "exec"), ns)
83+
except (Exception, SystemExit) as e: # noqa: BLE001 -- any failure is the result
84+
tb = traceback.extract_tb(e.__traceback__)[-1]
85+
line = f"{type(e).__name__}: {e} ({os.path.basename(tb.filename)}:{tb.lineno})"
86+
return phase, type(e).__name__, line
7587
return None
7688

7789

7890
def run_block(label, code):
7991
fresh_scene()
8092
try:
8193
exec(compile(code, label, "exec"), {"__name__": "__main__"})
82-
except Exception as e: # noqa: BLE001
94+
except (Exception, SystemExit) as e: # noqa: BLE001
8395
tb = traceback.extract_tb(e.__traceback__)[-1]
8496
return f"{type(e).__name__}: {e} (line {tb.lineno})"
8597
return None
@@ -109,16 +121,23 @@ def main():
109121
case = CASES[name]
110122
source = open(os.path.join(REPO, "snippets", name), encoding="utf-8").read()
111123
err = run_snippet(name, source, case)
112-
results.append(("snippet", name, "FAIL" if err else "PASS", err or ""))
124+
results.append(("snippet", name, "FAIL" if err else "PASS",
125+
f"[{err[0]}] {err[2]}" if err else ""))
113126
if args.no_falsify or err:
114127
continue
115128
old, new = case["falsify"]
116129
if old not in source:
117130
results.append(("falsifier", name, "FAIL", f"{old!r} not in source"))
118131
continue
119132
err = run_snippet(name, source.replace(old, new), case)
120-
results.append(("falsifier", name, "FALSIFIED" if err else "FAIL",
121-
err or f"check still passed with {old!r} -> {new!r}"))
133+
if err is None:
134+
outcome, reason = classify_content_falsifier(None, None)
135+
detail = f"{reason} with {old!r} -> {new!r}"
136+
else:
137+
phase, error_type, line = err
138+
outcome, reason = classify_content_falsifier(phase, error_type)
139+
detail = f"{reason}: {line}" if reason else line
140+
results.append(("falsifier", name, outcome, detail))
122141

123142
for label, code in skill_blocks():
124143
if args.only and args.only not in label:
@@ -129,6 +148,11 @@ def main():
129148
for kind, name, outcome, detail in results:
130149
print(f"{outcome:9} {kind:11} {name}" + (f" -- {detail}" if detail else ""))
131150
failed = [r for r in results if r[2] == "FAIL"]
151+
if not results:
152+
# an empty run proves nothing: a broken CASES import or a filter that
153+
# matched nothing must not report green
154+
print("run_content: no snippet or skill block ran")
155+
return 1
132156
count = lambda k: sum(1 for r in results if r[0] == k) # noqa: E731
133157
print(f"run_content: {count('snippet')} snippets, {count('falsifier')} falsifiers, "
134158
f"{count('skill-block')} skill blocks; {len(failed)} failed")

‎tests/smoke/test_harness.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,17 @@ def test_catalog_linter_shares_the_forbidden_set(self):
202202
from protocol import FORBIDDEN_FALSIFIER_EXITS
203203
self.assertEqual(check_smoke_catalog.FORBIDDEN_EXITS, set(FORBIDDEN_FALSIFIER_EXITS))
204204

205+
def test_content_falsifier_counts_only_a_failing_check(self):
206+
# #519: the edit must break the contract the check asserts, not the code
207+
from protocol import FALSIFIED, classify_content_falsifier as cf
208+
self.assertEqual(cf("check", "AssertionError")[0], FALSIFIED)
209+
self.assertEqual(cf("check", "RuntimeError")[0], FALSIFIED) # e.g. a bake that did not finish
210+
self.assertEqual(cf(None, None)[0], FAIL) # check still passed
211+
self.assertEqual(cf("snippet", "AttributeError")[0], FAIL) # edit crashed the snippet
212+
self.assertEqual(cf("setup", "AssertionError")[0], FAIL)
213+
for broken in ("NameError", "UnboundLocalError", "SyntaxError", "ImportError"):
214+
self.assertEqual(cf("check", broken)[0], FAIL, broken)
215+
205216
def test_legal_skip_stays_skip(self):
206217
out = "SMOKE_SKIP: needs 5.1\n"
207218
st, _ = classify(proc_exit=SKIP_EXIT, output=out, min_version="5.1",

0 commit comments

Comments
 (0)