[CI] (Pipe): gate that the buffer pool, the deferred-release drain and the three rings did not move

This commit is contained in:
2026-09-08 06:02:36 -04:00
parent 6e9943b36e
commit 4412cef00c
3 changed files with 602 additions and 11 deletions
+50 -11
View File
@@ -569,24 +569,25 @@ jobs:
working-directory: build-verify
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
# The two always-on P2 negative controls (G8, G12), which are labelled integration-gpu and
# not integration-verify - they are about the handle key and the CSO switch, not about the
# comparator - so the lane above does not reach them. They are run HERE because this is the
# only CI job that unpacks a MOBILEGL_PIPE_PUSH build: CsoContentAddressingScenario reads
# the two CSO counters out of the library's summary line and both the counters and the
# cso[] bracket are #if MOBILEGL_PIPE_PUSH, so in the pull `integration` job the entries do
# not exist at all.
# The always-on P2 and P3a negative controls (G8, G10, G12), which are labelled
# integration-gpu and not integration-verify - they are about the handle key, the subsystem
# switch and the map-persistent counter, not about the comparator - so the lane above does
# not reach them. They are run HERE because this is the only CI job that unpacks a
# MOBILEGL_PIPE_PUSH build: every one of them reads a counter out of the library's summary
# line, and both the counters and their brackets are #if MOBILEGL_PIPE_PUSH, so in the pull
# `integration` job the entries exist (gate G2 requires the same names in both builds) but
# have nothing to assert.
#
# An arm whose subsystem has not landed on this tree SKIPS with the reason (never absent,
# never a green that asserted nothing), so this step is green through the P2 landing order
# and starts asserting as each package arrives.
# never a green that asserted nothing), so this step is green through the P2 and P3a landing
# orders and starts asserting as each package arrives.
#
# The environment is the sibling step's, deliberately and in full: these entries run the
# same DirectVulkan binary through the same runner, so the three MOBILEGL_MAGMA_* fixes it
# needs apply here too, and a crash here has to leave a core for the same black-box flow.
# The step above is the only reason those lines exist in this job; a control that crashed
# without one would be the hardest failure in the job to diagnose.
- name: The handle-ABA and CSO-content-addressing controls (G8, G12)
- name: The handle-ABA, CSO and P3a subsystem controls (G8, G10, G12)
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
@@ -597,7 +598,8 @@ jobs:
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
ctest --output-on-failure -L integration-gpu \
-R 'HandleRecycle|CsoContentAddressing' --no-tests=error -j 4
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystemControl|MapPersistentRoundtrips' \
--no-tests=error -j 4
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
# library opens it fopen(path, "w"), so after an ambient lane of 400-odd processes the file
@@ -1540,10 +1542,21 @@ jobs:
runs-on: ubuntu-latest
# Deliberately independent of build-linux: these are source-level gates, they take
# seconds, and a broken build must not hide a drifted interface.
env:
# P3a's base ref, for the G5 region gate below. It is 44c2b5cf and NOT the workflow's
# baseline_sha input: that input is the SYMBOL baseline (087685d1, P1's G1 reading) and it
# is empty on a push, whereas this gate asks "did the buffer pool, the deferred-release
# drain and the three rings move since the phase started". INTEGRATOR-DECISIONS ID-1 is
# where the ref comes from.
BASELINE: "44c2b5cf"
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
# The G5 gate reads Managers.cpp at BASELINE with `git show`, which a depth-1 checkout
# does not have. Nothing else in this job needs history.
fetch-depth: 0
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
# keeps the two interface tables, the wire records, the verify comparators, the
@@ -1603,6 +1616,32 @@ jobs:
python3 scripts/gen_pipe_dirty_surface.py --check
python3 scripts/gen_pipe_dirty_surface.py --self-test
# A GATE as of P3a (G5). "pool 与延迟释放原样搬" (ROADMAP.md:19) is meant literally: the
# buffer pool, the deferred-release drain and the three persistently mapped rings move
# VERBATIM, and ARCHITECTURE.md:515 says why - their retire happens only inside Present, so
# a batching or ordering change there starves them, and nothing else in this workflow can
# see it. P3a rewrites the rest of Managers.cpp by design, so a file diff says nothing; the
# script extracts the nine named bodies and compares their hashes on their own.
#
# Scoped to the disaggregation branch and to a manual dispatch, deliberately: the question
# is "did these nine move since P3a started", and BASELINE is P3a's base ref. On dev, where
# unrelated buffer fixes land on their own schedule, the same comparison would be asking a
# question nobody posed - it belongs with the TEMPORARY trigger lines at the top of this
# file and retires with them.
#
# --self-test is the half that keeps it honest, and it is not optional: a comparison that
# silently stopped comparing produces exactly the same green as nine untouched bodies. It
# runs three canned controls - nine bodies extracted, an edit OUTSIDE them ignored, and a
# perturbed body reported BY NAME - and fails if any of them does not answer. Same shape as
# gen_pipe.py --self-test above.
- name: The buffer pool, the deferred-release drain and the rings did not move (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p3a_untouched_regions.sh "${BASELINE}" HEAD
- name: The untouched-region gate can still fail (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p3a_untouched_regions.sh --self-test
# Warning only for now: the disaggregation documents are still being written, and a
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
# --strict when the documents settle.
+344
View File
@@ -0,0 +1,344 @@
#!/usr/bin/env bash
# G5's gate: "pool 与延迟释放原样搬" is LITERAL - nine functions in
# MobileGL/MG_Backend/DirectGLES/Managers.cpp are byte-identical after P3a.
#
# WHAT G5 CLAIMS, and why a diff of the file cannot say it. ARCHITECTURE.md:316 puts the three
# persistently mapped rings and the buffer pool in the do-not-touch list, and :515 says what
# protects them: `present` and eglSwapBuffers are strictly 1:1, so the rings' and the pool's retire
# only ever happens inside Present, and a batching change would starve them. P3a rewrites the file
# those nine functions live in - Ops_* becomes handle-shaped, the twin's gate is re-keyed - so the
# file's diff is large by design and says nothing about whether the pool moved. This gate extracts
# the nine BODIES and compares them on their own.
#
# The nine, and what each one is (BRIEF-P3A.md D-F):
#
# IsPoolable takes the server-side resource, never the frontend object
# EnrollIntoPool the retireSerial = CurrentFrameSerial() + 1 stamp is load-bearing
# AcquireFromPool hands back only entries whose GPU work is complete
# TrimBufferPool called once per frame from Present
# ClearBufferPool context loss
# ProcessDeferredBufferReleases drained per draw, fast-outs on an atomic flag
# CreateRingStorage glBufferStorageEXT + persistent|coherent, retires at serial + 1
# RingAvailable self-heals a stale context generation
# RingAllocate the fast path on the hot upload route
#
# HOW A BODY IS EXTRACTED. The file is masked first - comments, string, char and raw-string
# literals are replaced by spaces of the same length, so a brace or a parenthesis inside one can
# never be counted - and the DEFINITION is then found as the one occurrence of `<name> (` whose
# closing parenthesis is followed (past qualifiers like const/noexcept) by `{`. That is what tells
# a definition from the forward declarations at the top of the anonymous namespace and from the
# call sites: a call's `)` is followed by `)`, `;` or `,`, never by `{`. The body is then brace
# matched in the masked text and hashed from the ORIGINAL text, so a comment change inside one of
# these functions is a difference too - which is deliberate: the claim is "byte-identical", and a
# comment that stopped describing what the code does is exactly the kind of drift a "verbatim
# move" is supposed to be checked for.
#
# Exactly one definition must be found per name. Zero or two is exit 2 (could not run), never a
# silent pass: a rename that this gate could not follow must not read as "nothing moved".
#
# Usage:
# scripts/p3a_untouched_regions.sh <ref-a> <ref-b> compare the nine bodies at two git refs
# scripts/p3a_untouched_regions.sh <ref> print the nine shas at one ref (the D.0
# baseline capture: ... > p3a-before-untouched.sha)
# scripts/p3a_untouched_regions.sh --self-test prove the comparison can go red
#
# stdout is always the sha list - `<sha256> <function>`, one per line, in the fixed order above -
# so the baseline capture is a plain redirect. Everything else goes to stderr.
#
# Both arguments are GIT REFS: the gate is about what landed, so an uncommitted edit is invisible
# by design. Use HEAD after committing, which is what D.1 and the CI row do.
#
# Exit codes: 0 the nine bodies are identical at both refs (or a single ref was listed);
# 1 at least one moved - the first one in the fixed order is named on stderr;
# 2 the gate could not run: a bad ref, a missing file, a name that is not defined
# exactly once, or a self-test whose control failed to trip.
set -u -o pipefail
SOURCE_PATH=MobileGL/MG_Backend/DirectGLES/Managers.cpp
FUNCTIONS="IsPoolable EnrollIntoPool AcquireFromPool TrimBufferPool ClearBufferPool ProcessDeferredBufferReleases CreateRingStorage RingAvailable RingAllocate"
# The function the self-test perturbs. Any of the nine would do; this one is small, has no
# forward declaration and no overload, so a failure to trip is about the comparison rather than
# about the extraction.
SELF_TEST_FUNCTION=ClearBufferPool
say() { echo "[p3a-untouched] $*" >&2; }
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) || exit 2
cd "$REPO_ROOT" || exit 2
WORK_DIR=$(mktemp -d) || exit 2
trap 'rm -rf "$WORK_DIR"' EXIT
# The extractor. Two modes, both over a FILE so the self-test can drive it without inventing a
# commit: `extract` prints one `<sha> <name>` line per function, `perturb` writes a copy of the
# file with one statement inserted at the top of one function's body.
PY=$WORK_DIR/extract.py
cat > "$PY" <<'PYTHON'
import hashlib
import re
import sys
def mask(text):
"""Comments and literals replaced by spaces of the same length, offsets preserved."""
out = list(text)
i, n = 0, len(text)
while i < n:
c = text[i]
if c == '/' and i + 1 < n and text[i + 1] == '/':
while i < n and text[i] != '\n':
out[i] = ' '
i += 1
elif c == '/' and i + 1 < n and text[i + 1] == '*':
out[i] = out[i + 1] = ' '
i += 2
while i + 1 < n and not (text[i] == '*' and text[i + 1] == '/'):
if text[i] != '\n':
out[i] = ' '
i += 1
if i + 1 < n:
out[i] = out[i + 1] = ' '
i += 2
elif c == 'R' and i + 1 < n and text[i + 1] == '"':
# R"delim( ... )delim" - a shader source is one of these, and it is full of braces.
close = text.find('(', i + 2)
if close < 0:
i += 1
continue
delim = text[i + 2:close]
end = text.find(')' + delim + '"', close)
end = n if end < 0 else end + len(delim) + 2
for j in range(i, end):
if text[j] != '\n':
out[j] = ' '
i = end
elif c in '"\'':
quote = c
out[i] = ' '
i += 1
while i < n and text[i] != quote:
if text[i] == '\\' and i + 1 < n:
out[i] = ' '
i += 1
if text[i] != '\n':
out[i] = ' '
i += 1
if i < n:
out[i] = ' '
i += 1
else:
i += 1
return ''.join(out)
def match_forward(masked, start, opener, closer):
depth = 0
for i in range(start, len(masked)):
if masked[i] == opener:
depth += 1
elif masked[i] == closer:
depth -= 1
if depth == 0:
return i
return -1
def find_definition(text, masked, name):
"""(begin, end) of the ONE definition of `name`, or a reason it could not be found."""
hits = []
for m in re.finditer(r'\b' + re.escape(name) + r'\s*\(', masked):
open_paren = m.end() - 1
close_paren = match_forward(masked, open_paren, '(', ')')
if close_paren < 0:
continue
tail = masked[close_paren + 1:close_paren + 96]
# Past whatever qualifiers a definition may carry; anything else means this was a call
# or a declaration.
stripped = re.sub(r'^(\s|const\b|noexcept\b|override\b|final\b)*', '', tail)
if not stripped.startswith('{'):
continue
brace = masked.index('{', close_paren)
end = match_forward(masked, brace, '{', '}')
if end < 0:
continue
begin = text.rfind('\n', 0, m.start()) + 1
hits.append((begin, end + 1))
return hits
def extract(path, names):
text = open(path, encoding='utf-8', newline='').read()
masked = mask(text)
rows, problems = [], []
for name in names:
hits = find_definition(text, masked, name)
if len(hits) != 1:
problems.append('%s: expected exactly one definition, found %d' % (name, len(hits)))
continue
begin, end = hits[0]
body = text[begin:end]
rows.append((hashlib.sha256(body.encode('utf-8')).hexdigest(), name))
return rows, problems
def perturb(src, dst, names, target):
text = open(src, encoding='utf-8', newline='').read()
masked = mask(text)
hits = find_definition(text, masked, target)
if len(hits) != 1:
sys.stderr.write('[p3a-untouched] cannot perturb %s: %d definitions\n' % (target, len(hits)))
return 2
begin, end = hits[0]
brace = text.index('{', begin)
patched = (text[:brace + 1] +
'\n // p3a_untouched_regions.sh --self-test: a body that MOVED.\n' +
text[brace + 1:])
open(dst, 'w', encoding='utf-8', newline='').write(patched)
return 0
def main(argv):
mode = argv[1]
names = argv[-1].split()
if mode == 'extract':
rows, problems = extract(argv[2], names)
for problem in problems:
sys.stderr.write('[p3a-untouched] %s\n' % problem)
for sha, name in rows:
sys.stdout.write('%s %s\n' % (sha, name))
return 2 if problems else 0
if mode == 'perturb':
return perturb(argv[2], argv[3], names, argv[4])
sys.stderr.write('[p3a-untouched] unknown mode %r\n' % mode)
return 2
sys.exit(main(sys.argv))
PYTHON
# Extract the nine bodies at a git ref into "$2".
extract_ref() {
local ref=$1 out=$2 blob="$WORK_DIR/$2.cpp"
if ! git show "$ref:$SOURCE_PATH" > "$blob" 2>"$WORK_DIR/show.err"; then
say "cannot read $SOURCE_PATH at '$ref':"
sed 's/^/[p3a-untouched] /' "$WORK_DIR/show.err" >&2
return 2
fi
python3 "$PY" extract "$blob" "$FUNCTIONS" > "$WORK_DIR/$out.sha"
return $?
}
# Compare two sha lists. Prints the first function that moved.
compare_lists() {
local a=$1 b=$2 labelA=$3 labelB=$4 moved=0
while read -r shaA name; do
local shaB
shaB=$(awk -v n="$name" '$2 == n { print $1 }' "$b")
if [ "$shaA" != "$shaB" ]; then
if [ "$moved" -eq 0 ]; then
say "FIRST FUNCTION THAT MOVED: $name"
say " $labelA $shaA"
say " $labelB ${shaB:-<not found>}"
say " G5 (ARCHITECTURE.md:316, :515) says the buffer pool, the deferred-release drain and"
say " the three rings move VERBATIM. If this change is intended, it is not a P3a change and"
say " it needs its own commit and its own reason; if it is not, revert the body."
else
say "also moved: $name"
fi
moved=$((moved + 1))
fi
done < "$a"
return $((moved > 0 ? 1 : 0))
}
# --- self-test ------------------------------------------------------------------------------
# A gate that always says "identical" and a gate that is working produce the same green, so the
# comparison has to be shown failing. Both controls run: the POSITIVE one (an untouched copy
# compares equal) rules out a comparison that reports every function as moved, and the NEGATIVE
# one (one body perturbed) rules out the comparison that never reports any.
if [ "${1:-}" = "--self-test" ]; then
[ $# -eq 1 ] || { say "--self-test takes no other arguments"; exit 2; }
[ -f "$SOURCE_PATH" ] || { say "$SOURCE_PATH is not in this tree"; exit 2; }
cp -f "$SOURCE_PATH" "$WORK_DIR/pristine.cpp" || exit 2
if ! python3 "$PY" extract "$WORK_DIR/pristine.cpp" "$FUNCTIONS" > "$WORK_DIR/pristine.sha"; then
say "the extractor could not read the nine bodies out of the working tree's $SOURCE_PATH"
exit 2
fi
found=$(wc -l < "$WORK_DIR/pristine.sha")
if [ "$found" -ne 9 ]; then
say "extracted $found bodies, expected 9"
exit 2
fi
say "positive control: 9 bodies extracted from the working tree"
cp -f "$WORK_DIR/pristine.cpp" "$WORK_DIR/copy.cpp"
python3 "$PY" extract "$WORK_DIR/copy.cpp" "$FUNCTIONS" > "$WORK_DIR/copy.sha" || exit 2
if ! compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/copy.sha" "pristine" "copy" 2>/dev/null; then
say "POSITIVE CONTROL FAILED: an untouched copy compared as MOVED. The comparison is reporting"
say "differences that are not there, so its verdict means nothing in either direction."
exit 2
fi
say "positive control: an untouched copy compares equal"
# The second positive control, and it is the one that matters for P3a: the rest of this file
# IS going to be rewritten (the Ops_* become handle-shaped, the twin's gate is re-keyed), so a
# gate that fired on any edit to Managers.cpp would have to be switched off in the same week it
# landed. An edit outside the nine bodies must be invisible here.
{ echo "// p3a_untouched_regions.sh --self-test: an edit OUTSIDE the nine bodies."; \
cat "$WORK_DIR/pristine.cpp"; } > "$WORK_DIR/outside.cpp"
python3 "$PY" extract "$WORK_DIR/outside.cpp" "$FUNCTIONS" > "$WORK_DIR/outside.sha" || exit 2
if ! compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/outside.sha" "pristine" "outside" \
2>/dev/null; then
say "POSITIVE CONTROL FAILED: an edit OUTSIDE the nine bodies was reported as one of them"
say "moving. This gate would fire on every P3a commit to Managers.cpp and would have to be"
say "silenced, which is the same as not having it."
exit 2
fi
say "positive control: an edit outside the nine bodies is invisible"
python3 "$PY" perturb "$WORK_DIR/pristine.cpp" "$WORK_DIR/perturbed.cpp" \
"$SELF_TEST_FUNCTION" "$FUNCTIONS" || exit 2
python3 "$PY" extract "$WORK_DIR/perturbed.cpp" "$FUNCTIONS" > "$WORK_DIR/perturbed.sha" || exit 2
if compare_lists "$WORK_DIR/pristine.sha" "$WORK_DIR/perturbed.sha" "pristine" "perturbed" \
2> "$WORK_DIR/perturbed.err"; then
say "NEGATIVE CONTROL DID NOT TRIP: $SELF_TEST_FUNCTION's body was changed and the comparison"
say "still reported every function as identical. This gate cannot go red for the reason it"
say "exists, so every green it has ever printed means nothing."
exit 2
fi
if ! grep -q "FIRST FUNCTION THAT MOVED: $SELF_TEST_FUNCTION" "$WORK_DIR/perturbed.err"; then
say "NEGATIVE CONTROL TRIPPED FOR THE WRONG REASON: the comparison went red but did not name"
say "$SELF_TEST_FUNCTION as the first function that moved. It said:"
sed 's/^/[p3a-untouched] /' "$WORK_DIR/perturbed.err" >&2
exit 2
fi
say "negative control: a perturbed $SELF_TEST_FUNCTION body is reported, and named"
say "self-test passed"
exit 0
fi
# --- the gate -------------------------------------------------------------------------------
case $# in
1)
extract_ref "$1" one || exit 2
cat "$WORK_DIR/one.sha"
say "listed the nine bodies at $1"
exit 0
;;
2) ;;
*)
say "usage: $0 <ref-a> <ref-b> | $0 <ref> | $0 --self-test"
exit 2
;;
esac
extract_ref "$1" a || exit 2
extract_ref "$2" b || exit 2
cat "$WORK_DIR/b.sha"
if compare_lists "$WORK_DIR/a.sha" "$WORK_DIR/b.sha" "$1" "$2"; then
say "the nine pool / deferred-release / ring functions are byte-identical between $1 and $2"
exit 0
fi
exit 1
@@ -0,0 +1,208 @@
#!/usr/bin/env bash
# G7's negative control for P3a: drop one field from the vertex-input wire conversion on purpose
# and prove the emission-consistency suite says so, naming the field.
#
# WHAT G6 CLAIMS. For every VAO configuration the client's emitted MGPVertexElements blob +
# MGPVertexBuffers set + MGPIndexBuffer reproduce EXACTLY the values
# BackendVertexArrayObject::SyncToBackend reads from the frontend today, field by field, for all 32
# attribute slots. MG_Test/Pipe/VertexInputEmitTest.cpp (suite `VertexInputEmit`) is what walks it.
#
# WHY A CONTROL IS NEEDED AT ALL. That suite is green on a correct conversion, and it would be just
# as green on a conversion it had stopped looking at: a walk that drove no configurations, a
# comparison that stopped reading the blob, an assertion someone loosened. Green says nothing about
# whether the suite can still fail. This script makes it fail, for the one reason it exists to
# catch, and reports a NON-ZERO ctest THAT NAMES THE DROPPED FIELD as the pass.
#
# THE BREAK. `IsBgra` stops being copied into MGPVertexAttribWire. It is deliberately a break the
# COMPILER CANNOT SEE: the struct still has the member, MGPVertexAttribWire is still 24 bytes, its
# four static_asserts in MGPipeValueTypes.h's trip-wire block still hold, the field list in
# PipeFields.def still names it and the generated comparator still compares it. What breaks is the
# VALUE - the wire record now says "not BGRA" for a GL_BGRA attribute, which is the shape that made
# a swizzled colour array read back with its channels in driver order. It is also the field with
# the least other coverage: `Size` keeps 4 for GL_BGRA (D-G2), so a dropped IsBgra does not even
# change the attribute's size.
#
# The patch is applied by REGEX rather than by an exact line, because MG_Impl/Pipe/VertexInputEmit.h
# is package B's file and its spelling is B's to choose: any `<something>.IsBgra = <expr>;` (or the
# designated-initializer `.IsBgra = <expr>,`) has its right-hand side replaced by 0. If the header
# does not assign the field at all - because it has not landed yet, or because the conversion is
# spelled some other way - that is exit 2, "could not run", never a pass.
#
# WHY IT IS NOT A CI LANE. It rebuilds the library twice. It is run by hand, and by the integrator
# at the P3a five-part gate (D.3, beside P2's g7_negative_control.sh).
#
# Usage:
# scripts/p3a_vertex_input_negative_control.sh <build-dir>
#
# <build-dir> a configured build directory carrying the push-only unit suites
# (VertexInputEmit's emission cases are compiled only under MOBILEGL_PIPE_PUSH)
#
# Exit codes: 0 the control tripped AND named IsBgra;
# 1 the control did not answer: either the suite stayed green with the field dropped,
# or it went red without ever naming IsBgra, so the red cannot be attributed to the
# dropped field. Both are findings about the TEST, not errors in this script - and
# both leave the tree restored and rebuilt;
# 2 the script could not run the control at all (bad arguments; the header or the
# field is absent on this tree; no matching test; a build that was already broken;
# a failed restore).
set -u -o pipefail
BUILD_DIR=""
while [ $# -gt 0 ]; do
case "$1" in
-*) echo "unknown arg: $1" >&2; exit 2 ;;
*) BUILD_DIR=$1; shift ;;
esac
done
[ -n "$BUILD_DIR" ] || { echo "usage: $0 <build-dir>" >&2; exit 2; }
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) || exit 2
cd "$REPO_ROOT" || exit 2
[ -f "$BUILD_DIR/CMakeCache.txt" ] || { echo "$BUILD_DIR is not a configured build directory" >&2; exit 2; }
HEADER=MobileGL/MG_Impl/Pipe/VertexInputEmit.h
FIELD=IsBgra
TEST_NAME='VertexInputEmit\.'
LOG_DIR=$(mktemp -d)
BACKUP=$LOG_DIR/VertexInputEmit.h.orig
say() { echo "[p3a-g7] $*" >&2; }
restore() {
# From the byte-for-byte copy taken before the patch, never from git: someone running this on a
# dirty tree must get their own tree back, not HEAD.
if [ -f "$BACKUP" ]; then cp -f "$BACKUP" "$HEADER"; fi
}
# --- 0. the control has to have something to break ------------------------------------------
# On the P3a contract tree this is where the script stops: package B owns VertexInputEmit.h and it
# does not exist yet. Saying so is the honest report - a control that "passed" because there was
# nothing to break would be the worst outcome available here.
if [ ! -f "$HEADER" ]; then
say "$HEADER does not exist on this tree."
say "It is P3a package B's file (BRIEF-P3A.md C.5): the client-side vertex-input emitter, which"
say "is where the MGPVertexAttribWire conversion lives. Until it lands there is no field copy to"
say "drop, so this control cannot run and MUST NOT report a pass. Re-run on a tree that carries"
say "package B."
exit 2
fi
if ! grep -qE "\.${FIELD}[[:space:]]*=" "$HEADER"; then
say "$HEADER exists but assigns no .${FIELD}."
say "Either the wire conversion moved out of this header, or it does not copy ${FIELD} at all -"
say "and the second one would mean G6 is already broken in the way this control is supposed to"
say "create. Neither is something this script may report as a pass; look at the header."
exit 2
fi
# --- 1. the suite has to exist, and be green, BEFORE the break -------------------------------
# A missing test is NOT a pass: without this the script would patch, watch ctest match nothing,
# read that as "the test failed" and report the control as tripped.
matched=$(ctest --test-dir "$BUILD_DIR" -N -R "$TEST_NAME" 2>/dev/null | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched:-0}" -eq 0 ]; then
say "no test matches $TEST_NAME in $BUILD_DIR."
say "The suite is MG_Test/Pipe/VertexInputEmitTest.cpp (suite name VertexInputEmit, registered by"
say "the P3a contract commit). Until it carries the emission cases this control has nothing to"
say "trip and cannot report a pass."
exit 2
fi
say "$matched matching test(s) before the patch"
say "building $BUILD_DIR as it is"
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" > "$LOG_DIR/build-before.log" 2>&1; then
say "the build is already broken before any patch - see $LOG_DIR/build-before.log"
tail -20 "$LOG_DIR/build-before.log" >&2
exit 2
fi
if ! ctest --test-dir "$BUILD_DIR" -R "$TEST_NAME" --no-tests=error --output-on-failure \
> "$LOG_DIR/ctest-before.log" 2>&1; then
say "$TEST_NAME is already red before the patch - fix that first, the control proves nothing here"
tail -30 "$LOG_DIR/ctest-before.log" >&2
exit 2
fi
say "$TEST_NAME is green before the patch"
# --- 2. stop copying IsBgra ------------------------------------------------------------------
cp -f "$HEADER" "$BACKUP" || exit 2
trap 'restore' EXIT
say "dropping the ${FIELD} copy from $HEADER"
python3 - "$HEADER" "$FIELD" <<'PY' || exit 2
import re
import sys
path, field = sys.argv[1], sys.argv[2]
text = open(path, encoding='utf-8').read()
# `<lhs>.IsBgra = <expr>;` and the designated-initializer `.IsBgra = <expr>,`. The right-hand side
# is replaced rather than the line deleted, so the record still HAS the field and the break stays
# one the compiler cannot see.
pattern = re.compile(r'(\.' + re.escape(field) + r'\s*=\s*)([^;,\n]+)([;,])')
patched, count = pattern.subn(r'\g<1>0 /* G7 NEGATIVE CONTROL: was \g<2> */\g<3>', text)
if count == 0:
sys.stderr.write('[p3a-g7] no assignment to .%s to patch - the header changed shape since this '
'control was written; update the control, do not delete it.\n' % field)
sys.exit(1)
open(path, 'w', encoding='utf-8', newline='\n').write(patched)
print('[p3a-g7] neutralised %d assignment(s) to .%s' % (count, field))
PY
# --- 3. it must still COMPILE ----------------------------------------------------------------
# A build break here would prove the static_asserts work, not that the suite still checks.
say "rebuilding with the dropped field"
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" > "$LOG_DIR/build-after.log" 2>&1; then
say "the patched header did not compile, so the control cannot tell 'the test failed' from"
say "'nothing was built'. The break is supposed to be invisible to the compiler - if the field is"
say "read somewhere that needs its value, say so in the control rather than working around it."
grep -m10 -E 'error:' "$LOG_DIR/build-after.log" >&2
exit 2
fi
say "the patched header still compiles, so the record still has the field and still asserts its size"
# --- 4. the suite must now be RED, and name the field ----------------------------------------
say "running $TEST_NAME against the dropped field"
if ctest --test-dir "$BUILD_DIR" -R "$TEST_NAME" --no-tests=error --output-on-failure \
> "$LOG_DIR/ctest-after.log" 2>&1; then
say "NEGATIVE CONTROL DID NOT TRIP: $TEST_NAME is still green with ${FIELD} no longer copied into"
say "MGPVertexAttribWire. A GL_BGRA attribute now travels as a non-BGRA one and the emission"
say "comparison did not notice, so G6 is not checking what it claims to check."
cp -f "$LOG_DIR/ctest-after.log" ./p3a-vertex-input-control-failure.log
say "ctest output kept at ./p3a-vertex-input-control-failure.log"
exit 1
fi
# A red is not yet a pass: a suite that had started failing for an unrelated reason satisfies the
# first half of the claim and none of the second. The answer is remembered here and decided at the
# end - AFTER the restore, because leaving a build directory holding the broken header is worse
# than any exit status.
TRIPPED_FOR_THE_RIGHT_REASON=1
if grep -q "$FIELD" "$LOG_DIR/ctest-after.log"; then
say "negative control tripped, naming $FIELD"
else
TRIPPED_FOR_THE_RIGHT_REASON=0
say "negative control tripped, but its output does not name $FIELD - the suite failed for some"
say "other reason, so this is NOT a pass. Restoring first, then reporting it."
grep -m20 -E 'Failure|error|Expected|Actual' "$LOG_DIR/ctest-after.log" >&2
fi
# --- 5. put it back, and prove it went back --------------------------------------------------
restore
trap - EXIT
say "restored; rebuilding"
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" > "$LOG_DIR/build-restored.log" 2>&1; then
say "the tree did NOT rebuild after the restore - see $LOG_DIR/build-restored.log"
exit 2
fi
if ! ctest --test-dir "$BUILD_DIR" -R "$TEST_NAME" --no-tests=error \
> "$LOG_DIR/ctest-restored.log" 2>&1; then
say "the tree did NOT go back to green after the restore - see $LOG_DIR/ctest-restored.log"
exit 2
fi
if [ "$TRIPPED_FOR_THE_RIGHT_REASON" = 0 ]; then
cp -f "$LOG_DIR/ctest-after.log" ./p3a-vertex-input-control-wrong-reason.log
say "INCONCLUSIVE: $TEST_NAME went red with ${FIELD} dropped but never named it, so the red"
say "cannot be attributed to the dropped field. The tree is restored and green again; the failing"
say "output is kept at ./p3a-vertex-input-control-wrong-reason.log."
exit 1
fi
say "negative control tripped and the tree is green again"
exit 0