mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[CI] (Pipe): gate that the buffer pool, the deferred-release drain and the three rings did not move
This commit is contained in:
+50
-11
@@ -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.
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user