[Fix] (scripts): make gen_pipe.py's expect_trip require the tripped guard's own message and a non-zero exit code, with an R-16 meta-control that an unrelated SystemExit or sys.exit(0) cannot pass as a trip - the any-SystemExit defect fb70704e fixed in the field-ownership generator, confirmed still present here by codex review finding 9

This commit is contained in:
2026-09-16 06:33:07 -04:00
parent ce53553c79
commit 7110cdb61a
+70 -12
View File
@@ -1376,27 +1376,54 @@ def write(path, text, check, changed):
handle.write(text) handle.write(text)
def expect_trip(name, fn): def expect_trip(name, because, fn, quiet=False):
"""Runs one negative control; a gate that lets it through is the failure.""" """Runs one negative control; a gate that lets it through, or trips for a reason that is
not ITS OWN, is the failure.
The first version of this function caught any SystemExit - including an unrelated
diagnostic, and even a CLEAN sys.exit(0) - and asked nothing about which one, so the
codex cross-family review's finding 9 could replace a control's callback with either and
the "nine negative controls all trip" line stayed true while the guard it named never
fired. gen_pipe_field_ownership.py's M-1 was the same defect in the sibling generator;
this mirrors that fix.
`because` is a substring the control's OWN message must contain, and an exit code of 0
can never count as a trip."""
try: try:
fn() fn()
except SystemExit as trip: except SystemExit as trip:
print("gen_pipe: self-test %s: tripped as expected (%s)" % (name, str(trip).splitlines()[0][:100])) code = trip.code
return 1 message = str(code) if code is not None else ""
print("gen_pipe: self-test %s: DID NOT TRIP" % name, file=sys.stderr) if code != 0 and because in message:
if not quiet:
print("gen_pipe: self-test %s: tripped as expected (%s)"
% (name, message.splitlines()[0][:100]))
return 1
if not quiet:
print("gen_pipe: self-test %s: tripped for SOMEONE ELSE'S reason:\n"
" expected to contain: %s\n"
" actually said (code=%r): %s"
% (name, because, code, message.splitlines()[0][:200] if message else "<empty>"),
file=sys.stderr)
return 0
if not quiet:
print("gen_pipe: self-test %s: DID NOT TRIP" % name, file=sys.stderr)
return 0 return 0
def self_test(accessors): def self_test(accessors):
"""The negative controls (check_include_closure.py's shape): each gate must go red for """The negative controls (check_include_closure.py's shape): each gate must go red for
its reason, and zero trips is itself an error.""" its OWN reason, and zero trips is itself an error."""
canned_struct = "struct Canned {\n Uint32 A;\n Uint32 B, C;\n Uint8 Pad0[3];\n void F() { return; }\n};\n" canned_struct = "struct Canned {\n Uint32 A;\n Uint32 B, C;\n Uint8 Pad0[3];\n void F() { return; }\n};\n"
controls = [ controls = [
("struct member without F(...)", ("struct member without F(...)",
"member(s) with no F(...) in PipeFields.def",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])), lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B"]}, ["Canned"], [canned_struct])),
("F(...) that is not a member", ("F(...) that is not a member",
"F(...) name(s) that are not members",
lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])), lambda: check_field_lists_cover_struct_members({"Canned": ["A", "B", "C", "D"]}, ["Canned"], [canned_struct])),
("payload with no struct", ("payload with no struct",
"struct not found",
lambda: check_field_lists_cover_struct_members({"Nowhere": ["A"]}, ["Nowhere"], [canned_struct])), lambda: check_field_lists_cover_struct_members({"Nowhere": ["A"]}, ["Nowhere"], [canned_struct])),
] ]
fill_text = read(os.path.join(PIPE_DIR, "FillPoints.def")) fill_text = read(os.path.join(PIPE_DIR, "FillPoints.def"))
@@ -1404,16 +1431,24 @@ def self_test(accessors):
field_row = re.compile(r"X\(\s*kDraw\s*,\s*GetBoundVertexArray\s*\)") field_row = re.compile(r"X\(\s*kDraw\s*,\s*GetBoundVertexArray\s*\)")
if not verb_row.search(fill_text) or not field_row.search(fill_text): if not verb_row.search(fill_text) or not field_row.search(fill_text):
sys.exit("gen_pipe: self-test: FillPoints.def lost the rows the controls edit") sys.exit("gen_pipe: self-test: FillPoints.def lost the rows the controls edit")
controls.append(("verb missing from FillPoints.def", lambda: parse_fill_points( controls.append(("verb missing from FillPoints.def",
"GLFunctionsTable member(s) without a verb row",
lambda: parse_fill_points(
accessors, text=verb_row.sub("", fill_text, count=1)))) accessors, text=verb_row.sub("", fill_text, count=1))))
controls.append(("verb that is not a GLFunctionsTable member", lambda: parse_fill_points( controls.append(("verb that is not a GLFunctionsTable member",
"verb(s) that are not GLFunctionsTable members",
lambda: parse_fill_points(
accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1)))) accessors, text=verb_row.sub("X(DrawArrays, kDraw) X(NotAVerb, kDraw)", fill_text, count=1))))
controls.append(("field row naming a non-accessor", lambda: parse_fill_points( controls.append(("field row naming a non-accessor",
"is not an accessor in Coverage.def",
lambda: parse_fill_points(
accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1)))) accessors, text=field_row.sub("X(kDraw, NotAnAccessor)", fill_text, count=1))))
# The EMITTED list's own gate: a row naming a call that is not in PipeCalls.def would # The EMITTED list's own gate: a row naming a call that is not in PipeCalls.def would
# generate an enumerator nothing can dispatch on. # generate an enumerator nothing can dispatch on.
calls_for_control = parse_calls() calls_for_control = parse_calls()
controls.append(("emitted row naming a call that does not exist", lambda: gen_emitted_by( controls.append(("emitted row naming a call that does not exist",
"which is not a call in PipeCalls.def",
lambda: gen_emitted_by(
[("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")]))) [("GetViewport", "SetDynamicState")], calls_for_control, [("GetViewport", "NotACall")])))
# P5 R-13.4's gate. The flags are now a GENERATED TABLE six packages read instead of six # P5 R-13.4's gate. The flags are now a GENERATED TABLE six packages read instead of six
# hard-coded copies, so a token that is not an MGPipeCallFlags enumerator has to stop the # hard-coded copies, so a token that is not an MGPipeCallFlags enumerator has to stop the
@@ -1421,13 +1456,36 @@ def self_test(accessors):
# kNone, the empty set, may not be OR'd with a real flag and quietly read as one. # kNone, the empty set, may not be OR'd with a real flag and quietly read as one.
flag_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kHasBlobb"]) flag_typo = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kHasBlobb"])
flag_kNone = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone", "kHasBlob"]) flag_kNone = Call(1, "Canned", "MGPHandleOnly", "kScreen", ["kNone", "kHasBlob"])
flag_typo_because = "which is not an MGPipeCallFlags enumerator"
controls.append(("call flag that is not an MGPipeCallFlags enumerator", controls.append(("call flag that is not an MGPipeCallFlags enumerator",
flag_typo_because,
lambda: check_call_flags_are_known([flag_typo]))) lambda: check_call_flags_are_known([flag_typo])))
controls.append(("kNone combined with a real flag", controls.append(("kNone combined with a real flag",
"combines kNone with",
lambda: check_call_flags_are_known([flag_kNone]))) lambda: check_call_flags_are_known([flag_kNone])))
# R-16 meta-control (codex review finding 9; verified by execution in
# p5-results/wave1-codex-verify.md §9). The exact perturbation there replaced the
# flag-typo control's callback with `sys.exit("unrelated parser failure")` and,
# separately, with `sys.exit(0)`; the OLD expect_trip counted both as "tripped as
# expected" because it never looked at the message or the code. Both must be REJECTED
# here, under the real control's own `because`. This is a quiet, unlisted assertion (not
# one of the nine controls below) so it cannot itself inflate the trip count.
# "Red once by doing X" = reverting expect_trip to `except SystemExit as trip: return 1`
# (accept any SystemExit) - that alone turns each of these two checks from a silent pass
# into the sys.exit below.
if expect_trip("(R-16 meta-control) unrelated diagnostic standing in for the flag-typo guard",
flag_typo_because, lambda: sys.exit("unrelated parser failure"), quiet=True) != 0:
sys.exit("gen_pipe: self-test: expect_trip counted an UNRELATED SystemExit as the "
"flag-typo guard's own trip - finding 9 / R-16 is back")
if expect_trip("(R-16 meta-control) sys.exit(0) standing in for the flag-typo guard",
flag_typo_because, lambda: sys.exit(0), quiet=True) != 0:
sys.exit("gen_pipe: self-test: expect_trip counted sys.exit(0) as a trip - "
"finding 9 / R-16 is back")
trips = 0 trips = 0
for name, fn in controls: for name, because, fn in controls:
trips += expect_trip(name, fn) trips += expect_trip(name, because, fn)
# The positive control: the canned struct's exact list passes, and the parser sees the # The positive control: the canned struct's exact list passes, and the parser sees the
# padding member as padding and the function as not a member. # padding member as padding and the function as not a member.
check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct]) check_field_lists_cover_struct_members({"Canned": ["A", "B", "C"]}, ["Canned"], [canned_struct])