[CI] (Pipe): gate that the unpack ring, the attachment permutation, the depth-stencil sampling core and the format caveat did not move

This commit is contained in:
2026-09-08 19:08:12 -04:00
committed by rereview
parent 8c8d460e14
commit 2f35014d73
3 changed files with 1052 additions and 8 deletions
+66 -8
View File
@@ -601,7 +601,26 @@ jobs:
# only in the pull `integration` job, where a MOBILEGL_PIPE_PUSH value steers nothing
# (Config.h declares the field inside the push guard) and both arms are the same legacy path.
# A lane that cannot go red where it is installed is not a gate (ROADMAP.md:7).
- name: The handle-ABA, CSO and P3a subsystem controls (G8, G10, G12)
# P4a ADDS THREE ALTERNATIVES, and each one is here because this is the only CI job that
# unpacks a push build:
# * `ObjectSubsystem` reaches the three DirectGLES.ObjectSubsystemControl. entries - the
# 0x1fff-vs-0x1ff A/B and the 0x9ff dependency refusal (G12). `ResourceSubsystem` does
# NOT match it: the two families are named apart on purpose, because they are different
# phases' switches and a filter that merged them would hide one behind the other.
# * `TextureParamsWithoutASamplerView` reaches G9's four cases, the scenario ROADMAP.md:20
# names by hand. It runs in the ambient lanes, which the label already selects - but this
# step is where those cases run against a PUSH library, and G9's whole claim is about the
# push path. All four cases are green on the contract commit - including the one D-E3
# expected to be red, for the reason the scenario's header records - so this row is green
# from the day it lands and goes red only if a reachability path stops syncing texture
# parameters at all, which is the coupling ARCHITECTURE.md:100 exists to remove.
# * `TextureUploadShape` is RECORDED, not gated (D-D4): it asserts that the two upload-shape
# counters could be read and that they agree, and prints the shape for MEASUREMENTS.md. It
# is in the filter so that the number is actually collected on every run - an unmeasured
# shape is not a recorded one - and because its own assertions can go red.
# As with the four before them, each alternative is the SHORTEST string that selects only what
# it means to.
- name: The handle-ABA, CSO, subsystem and texture-parameter controls (G8, G8b, G9, G10, G12)
working-directory: build-verify
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
@@ -612,7 +631,7 @@ jobs:
ulimit -c unlimited
sudo sysctl -w kernel.core_pattern='/tmp/core.%e.%p'
ctest --output-on-failure -L integration-gpu \
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystem|MapPersistentRoundtrip' \
-R 'HandleRecycle|CsoContentAddressing|ResourceSubsystem|MapPersistentRoundtrip|ObjectSubsystem|TextureParamsWithoutASamplerView|TextureUploadShape' \
--no-tests=error -j 4
# The arming lanes' logs, and ONLY those. Each lane shares one MOBILEGL_LOG_FILE_PATH and the
@@ -1557,12 +1576,18 @@ jobs:
# 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"
# THE CURRENT PHASE's base ref, for the two G5 region gates below. It is 37da3c3a - P4a's
# base ref, INTEGRATOR-DECISIONS ID-1 - 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 these
# gates ask "did the do-not-touch list move since the phase started".
#
# IT MOVED FROM P3a's 44c2b5cf TO P4a's 37da3c3a WITH THE PHASE, and that is a deliberate
# narrowing rather than a loss: P3a's eleven functions were compared against 44c2b5cf at P3a's
# own exit and were byte-identical there, so 37da3c3a carries the same bodies (measured: the
# eleven shas at 37da3c3a are the eleven shas at 44c2b5cf, and FlushPendingRangesFrom's is
# still the sha pinned in the script at 3e298c9a). What the two gates now both answer is "did
# anything on the list move during P4a", which is the question this phase can act on.
BASELINE: "37da3c3a"
steps:
- name: Checkout repo
@@ -1664,6 +1689,39 @@ jobs:
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p3a_untouched_regions.sh --self-test
# A GATE AS OF P4a (G5), and a SECOND script rather than an edit to the one above. P4a extends
# the same claim to the rest of ARCHITECTURE.md:318's do-not-touch list - the unpack PBO ring's
# staging repack and its two helpers, the attachment permutation, the D24S8 sampling-emulation
# core and the format-caveat handler - which is SEVENTEEN regions across THREE files
# (BRIEF-P4A.md D-N: P3a's eleven, which P4a must not touch either, plus P4a's six). The
# parent's SOURCE_PATH is a single file, so the extension needed a per-region source path and
# a region KIND (DepthStencilSamplingReadImpl is a namespace, not a function); everything else
# about the extraction is its parent's, verbatim.
#
# Both scripts run. The parent keeps answering its own question against its own eleven, so a
# regression in either half names itself, and neither gate can be silenced by editing the
# other's list.
#
# Same feat/disaggregated-or-dispatch guard as the P3a step, for the same reason: the question
# is "did these move since the phase started", and on dev - where unrelated buffer and texture
# fixes land on their own schedule - it would be 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 is not optional: a comparison that silently
# stopped comparing produces exactly the same green as seventeen untouched regions. It runs
# three positive controls (seventeen regions extracted, an untouched copy compared equal, an
# edit OUTSIDE them invisible in all three files) and FOUR negative ones - ClearBufferPool,
# FlushPendingRangesNow, RecomputeBackendColorSlots and StageBlocksIntoUnpackRing, each
# perturbed on its own and each required to be named BY NAME - and fails if any of them does
# not answer.
- name: The unpack ring, the attachment permutation, the D24S8 core and the format caveat did not move (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p4a_untouched_regions.sh "${BASELINE}" HEAD
- name: The P4a untouched-region gate can still fail (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p4a_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.
+345
View File
@@ -0,0 +1,345 @@
#!/usr/bin/env bash
# G7's negative controls for P4a: drop one field from each of two descriptor conversions on purpose
# and prove the emission-consistency suites say so, NAMING the field.
#
# WHAT G6 CLAIMS. For every framebuffer configuration, texture object, sampler object and program
# the client's emitted MGPFramebufferState / MGPResourceDesc / MGPTextureParams / MGPSamplerDesc /
# MGPSamplerView / MGPProgramDesc reproduce EXACTLY the values Espryt's SyncToBackend family reads
# from the frontend today, field by field. MG_Test/Pipe/{FramebufferEmit,TextureEmit,SamplerEmit,
# ImageEmit,ProgramEmit}Test.cpp are what walk it.
#
# WHY A CONTROL IS NEEDED AT ALL. Those suites are green on a correct conversion, and they would be
# just as green on a conversion they had stopped looking at: a walk that drove no configurations, a
# comparison that stopped reading the record, an assertion someone loosened. Green says nothing
# about whether a suite can still fail. This script makes each of them fail, for the one reason it
# exists to catch, and reports a NON-ZERO ctest THAT NAMES THE DROPPED FIELD as the pass.
#
# THE TWO BREAKS, one per new conversion family, each a break the COMPILER CANNOT SEE - the struct
# still has the member, the record is still its pinned size, the PipeFields.def row still names it
# and the generated comparator still compares it. What breaks is the VALUE:
#
# 1. MGPSurface::Layered stops being copied in MG_Impl/Pipe/FramebufferEmit.h. A layered
# attachment travels as a non-layered one, which is the shape that turns a whole-array render
# target into slice 0 - and SupportsLayeredImageBinding's rule (DirectGLES.cpp:1992-2013,
# D-O) forces `layer` to 0 for a non-layerable target, so the wrong answer is silently
# plausible rather than an error. It is also the field with the least other coverage: the
# surface's Res, Level and Layer are all still right, so nothing about the ATTACHMENT changes
# except the one bit that says how much of the texture it is.
# 2. SamplerParameters::borderColorForm stops being copied in MG_Impl/Pipe/SamplerEmit.h. All
# four border-colour VALUES still cross; what is lost is which of the three forms
# (float / int / uint) they are to be read as, and D-F4 is explicit that the form crosses and
# that all four values are compared. The failure it models is IntegerBorderColorScenario's:
# an integer border colour read as floats is not an error anywhere, it is just the wrong
# colour at the clamped edge of every sampled texture.
#
# Each patch is applied by REGEX rather than by an exact line, because both headers belong to OTHER
# PACKAGES (C.7: FramebufferEmit.h is package B's, SamplerEmit.h is package C's) and their spelling
# is theirs to choose: any `<something>.<field> = <expr>;` - or the designated-initializer
# `.<field> = <expr>,` - has its right-hand side replaced by a constant. If a header does not
# assign its field at all, because the emitter has not landed yet or because the conversion is
# spelled some other way, that is exit 2, "could not run", never a pass.
#
# ON THE P4a CONTRACT TREE THIS SCRIPT EXITS 2 AND SAYS SO. Both headers EXIST there - the contract
# commit creates all five emit headers with STUB emitters that return 0 payload bytes (contract-v1
# D1) - but neither assigns anything, so there is no field copy to drop. That is the honest report:
# a control that "passed" because there was nothing to break would be the worst outcome available.
#
# WHY IT IS NOT A CI LANE. It rebuilds the library up to four times. It is run by hand, and by the
# integrator at the P4a five-part gate (D.3 part 3, beside P2's g7_negative_control.sh and P3a's
# p3a_vertex_input_negative_control.sh, both of which keep running unchanged).
#
# Usage:
# scripts/p4a_descriptor_negative_control.sh <build-dir>
#
# <build-dir> a configured build directory carrying the push-only unit suites (the emission
# cases are compiled only under MOBILEGL_PIPE_PUSH)
#
# RESTORE IS NOT ENOUGH; THE REBUILD IS PART OF THE CONTRACT. Once a header has been patched, EVERY
# way out of this script goes through repair(): restore the header, rebuild the library from it, and
# re-run the suite to prove the tree really went back. `cp` alone leaves <build-dir> holding a
# libMobileGL.so in which the field is hard-wired, `ctest` does not rebuild, and nothing in an exit
# status tells a caller to. The path that needs this most is the one that reads "NEGATIVE CONTROL
# DID NOT TRIP": an engineer reacts to it by opening the suite and re-running ctest against this
# very build directory, and every reading they take there would come from a deliberately corrupted
# library. So repair() runs from the EXIT trap as well - a mid-way failure (a patched header that
# would not compile, an interrupt) repairs too - and a repair that itself fails downgrades the
# verdict to 2. On the exits that happen BEFORE any patch (bad arguments, a missing header, a
# missing suite) the tree was never touched and the build directory still holds what the caller
# built: there is nothing to restore and nothing to rebuild, and the script says which of the two
# situations it is leaving behind.
#
# Nothing is written into the repository. Logs and the header backups live in
# <build-dir>/p4a-g7-logs/, which is inside the build tree and therefore neither committed nor
# picked up by `git status`; the paths are printed with every verdict.
#
# Exit codes: 0 BOTH controls tripped AND named their field;
# 1 a control did not answer: a suite stayed green with its field dropped, or it went
# red without ever naming the field, so the red cannot be attributed to the drop.
# Both are findings about the TEST, not errors in this script - and both leave the
# tree restored AND rebuilt AND re-run;
# 2 a control could not be run at all (bad arguments; a header or a field absent on
# this tree; no matching test; a build that was already broken; a patched header that
# did not compile; a failed restore or a failed rebuild after one). Exit 2 wins over
# exit 1: "could not run" is never reported as "did not answer".
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; }
# One row per control: <header>@<field>@<ctest regex>@<owning package>. The ctest regex is the
# SHORTEST string that selects only that suite, the way the CI filters are written.
CONTROLS="\
MobileGL/MG_Impl/Pipe/FramebufferEmit.h@Layered@FramebufferEmit\.@B (clientfb)
MobileGL/MG_Impl/Pipe/SamplerEmit.h@borderColorForm@SamplerEmit\.@C (clientsp)"
# Inside the build tree, never in the repository: a run must not leave untracked files behind, and
# .gitignore carries no rule for a p4a-*.log at the root. Falls back to a temp directory only if
# the build directory cannot be written, which would be a strange build directory.
LOG_DIR=$(cd "$BUILD_DIR" && pwd)/p4a-g7-logs
mkdir -p "$LOG_DIR" 2>/dev/null || LOG_DIR=$(mktemp -d) || exit 2
say() { echo "[p4a-g7] $*" >&2; }
# --- the repair, shared by every exit path ---------------------------------------------------
# PATCHED_HEADER is the one header currently patched, empty when the tree is pristine. Only one
# control is ever in flight at a time, deliberately: two simultaneous drops would make a red
# unattributable to either.
PATCHED_HEADER=""
PATCHED_BACKUP=""
PATCHED_TEST=""
REPAIR_RC=0
repair() {
[ -n "$PATCHED_HEADER" ] || return 0
local header=$PATCHED_HEADER backup=$PATCHED_BACKUP test=$PATCHED_TEST
# Cleared FIRST, so a repair that is re-entered (the explicit call, then the EXIT trap) does the
# work once and reports the same answer twice.
PATCHED_HEADER=""
# 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" || { REPAIR_RC=2; return 2; }; fi
say "restored $header; rebuilding $BUILD_DIR from it"
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"
say "THE BUILD DIRECTORY IS NOT TRUSTWORTHY: repair it before reading anything out of it."
REPAIR_RC=2
return 2
fi
if ! ctest --test-dir "$BUILD_DIR" -R "$test" --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"
REPAIR_RC=2
return 2
fi
say "the tree is restored, rebuilt and green again"
return 0
}
# INT and TERM as well as EXIT (p3a-g7 m3). A Ctrl-C during a rebuild would otherwise leave the
# patched header in the tree - bash runs no EXIT trap for an uncaught SIGINT - and the next thing
# that reader does is build, from a hard-wired field, with nothing saying so. The two extra traps
# repair and then re-raise with the default disposition, so the exit status still reports the
# signal. Armed for the whole run: before the first patch repair() is a no-op.
trap 'repair' EXIT
trap 'repair; trap - INT; kill -INT $$' INT
trap 'repair; trap - TERM; kill -TERM $$' TERM
# --- one control -----------------------------------------------------------------------------
# Echoes "tripped" / "did-not-trip" / "wrong-reason" / "could-not-run" on stdout; everything else
# goes to stderr. The tree is repaired before it returns, whatever the answer.
run_control() {
local header=$1 field=$2 test=$3 owner=$4
local tag matched
tag=$(basename "$header" .h)-$field
# 0. the control has to have something to break.
if [ ! -f "$header" ]; then
say "$header does not exist on this tree."
say "It is P4a package $owner's file (BRIEF-P4A.md C.5/C.7): the client-side emitter that"
say "carries the ${field} copy. Until it lands there is no field copy to drop, so this control"
say "cannot run and MUST NOT report a pass. Re-run on a tree that carries that package."
echo could-not-run
return
fi
if ! grep -qE "\.${field}[[:space:]]*=" "$header"; then
say "$header exists but assigns no .${field}."
say "On the P4a CONTRACT tree that is the expected answer: the contract commit creates all five"
say "emit headers with STUB emitters (contract-v1 D1) that return 0 payload bytes and copy"
say "nothing, and package $owner fills the body in. Later in the phase it means something else"
say "and worse - either the conversion moved out of this header, or it does not copy ${field}"
say "at all, and the second one would mean G6 is already broken in exactly the way this control"
say "is supposed to create. Neither is something this script may report as a pass; look at the"
say "header."
echo could-not-run
return
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" 2>/dev/null | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched:-0}" -eq 0 ]; then
say "no test matches $test in $BUILD_DIR."
say "The suite is registered by the P4a contract commit; until it carries the emission cases"
say "this control has nothing to trip and cannot report a pass."
echo could-not-run
return
fi
say "[$tag] $matched matching test(s) before the patch"
say "[$tag] building $BUILD_DIR as it is"
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" > "$LOG_DIR/build-before-$tag.log" 2>&1; then
say "[$tag] the build is already broken before any patch - see $LOG_DIR/build-before-$tag.log"
tail -20 "$LOG_DIR/build-before-$tag.log" >&2
echo could-not-run
return
fi
if ! ctest --test-dir "$BUILD_DIR" -R "$test" --no-tests=error --output-on-failure \
> "$LOG_DIR/ctest-before-$tag.log" 2>&1; then
say "[$tag] $test is already red before the patch - fix that first, the control proves nothing"
tail -30 "$LOG_DIR/ctest-before-$tag.log" >&2
echo could-not-run
return
fi
say "[$tag] $test is green before the patch"
# 2. stop copying the field.
PATCHED_BACKUP=$LOG_DIR/$(basename "$header").orig
PATCHED_TEST=$test
cp -f "$header" "$PATCHED_BACKUP" || { echo could-not-run; return; }
# Set BEFORE the patcher runs, not after: a python that died half-way through the write must
# still be repaired. The cost is one unnecessary rebuild in the case where the patcher matched
# nothing and the file is byte-identical (cp refreshes its mtime).
PATCHED_HEADER=$header
say "[$tag] dropping the ${field} copy from $header"
if ! python3 - "$header" "$field" <<'PY'
import re
import sys
path, field = sys.argv[1], sys.argv[2]
text = open(path, encoding='utf-8').read()
# `<lhs>.Layered = <expr>;` and the designated-initializer `.Layered = <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('[p4a-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('[p4a-g7] neutralised %d assignment(s) to .%s' % (count, field))
PY
then
say "[$tag] the patcher did not apply; the tree is repaired on the way out"
repair || true
echo could-not-run
return
fi
# 3. it must still COMPILE. A build break here would prove the static_asserts work, not that the
# suite still checks.
say "[$tag] rebuilding with the dropped field"
if ! cmake --build "$BUILD_DIR" -j "$(nproc)" > "$LOG_DIR/build-after-$tag.log" 2>&1; then
say "[$tag] the patched header did not compile, so the control cannot tell 'the test failed'"
say "from 'nothing was built'. The break is supposed to be invisible to the compiler - if the"
say "field is read somewhere that needs its value, say so in the control rather than working"
say "around it."
grep -m10 -E 'error:' "$LOG_DIR/build-after-$tag.log" >&2
repair || true
echo could-not-run
return
fi
say "[$tag] the patched header still compiles, so the record still has the field and its size"
# 4. the suite must now be RED, and NAME the field. The verdict is only RECORDED here; nothing is
# reported and nothing returns until step 5 has put the tree back, because all three outcomes
# leave the same corrupted build directory behind.
local verdict
say "[$tag] running $test against the dropped field"
if ctest --test-dir "$BUILD_DIR" -R "$test" --no-tests=error --output-on-failure \
> "$LOG_DIR/ctest-after-$tag.log" 2>&1; then
verdict=did-not-trip
elif awk '/: Failure$/ || /: error:/ { block = 1 } block { print } /^[[:space:]]*$/ { block = 0 }' \
"$LOG_DIR/ctest-after-$tag.log" | grep -q "$field"; then
# 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. Matched against the FAILING ASSERTIONS'
# OWN BLOCKS rather than the whole ctest log (p3a-g7 m3): gtest prints a failure as
# `<file>:<line>: Failure` followed by the compared expressions and their values, terminated by
# a blank line, and the field name appears among those expressions - so the block, not the
# line, is the right unit and the whole file is the wrong one. A case NAMED after the field, a
# skip reason quoting it or a compiler note echoed into the log would all have made "tripped"
# mean "the string exists somewhere in the output".
verdict=tripped
else
verdict=wrong-reason
fi
# 5. put it back, and prove it went back. SHARED BY ALL THREE OUTCOMES, and that is the whole
# point of doing it before the verdict is reported.
if ! repair; then
say "[$tag] the control's own verdict was '$verdict', but the repair failed, so that verdict is"
say "not what this run reports: a build directory that could not be put back is 'could not run'."
echo could-not-run
return
fi
case "$verdict" in
did-not-trip)
say "[$tag] NEGATIVE CONTROL DID NOT TRIP: $test was still green with ${field} no longer"
say "copied into the record. The emission comparison did not notice a field it claims to"
say "compare, so G6 is not checking what it claims to check."
say "The run's output is kept at $LOG_DIR/ctest-after-$tag.log"
;;
wrong-reason)
say "[$tag] INCONCLUSIVE: $test went red with ${field} dropped but never named it, so the red"
say "cannot be attributed to the dropped field. The suite failed for some other reason."
grep -m20 -E 'Failure|error|Expected|Actual' "$LOG_DIR/ctest-after-$tag.log" >&2
say "The run's output is kept at $LOG_DIR/ctest-after-$tag.log"
;;
tripped)
say "[$tag] negative control tripped, naming $field, and the tree is green again"
;;
esac
echo "$verdict"
}
# --- both controls, then one verdict ----------------------------------------------------------
# Both are ALWAYS run, even when the first could not run: "FramebufferEmit has no emitter yet and
# SamplerEmit does" is a different tree from "neither does", and an engineer reading this output
# needs to know which. The exit code is the worst of the two, with 2 (could not run) outranking
# 1 (did not answer).
WORST=0
SUMMARY=""
while IFS='@' read -r header field test owner; do
[ -n "$header" ] || continue
verdict=$(run_control "$header" "$field" "$test" "$owner")
SUMMARY="$SUMMARY
$field ($header): $verdict"
case "$verdict" in
tripped) ;;
could-not-run) WORST=2 ;;
*) [ "$WORST" -eq 2 ] || WORST=1 ;;
esac
done <<EOF
$CONTROLS
EOF
trap - EXIT
say "---- G7 (P4a descriptor emission) ----$SUMMARY"
case "$WORST" in
0) say "both controls tripped and named their field; exit 0" ;;
1) say "a control did not answer; exit 1" ;;
2) say "a control could not be run; exit 2 (this is the expected answer on the P4a contract tree,"
say "where both emit headers are the contract's stubs and copy nothing)" ;;
esac
exit "$WORST"
+641
View File
@@ -0,0 +1,641 @@
#!/usr/bin/env bash
# G5's gate for P4a: the Espryt do-not-touch list is LITERAL - SEVENTEEN regions across THREE
# files stay byte-identical after P4a.
#
# WHAT G5 CLAIMS, and why a diff of the files cannot say it. ARCHITECTURE.md:318 is the Espryt
# do-not-touch list and :321 names the ONE item moved out of it (the sub-rect upload decision and
# the stride computation, which is P4a's D-D3/D-D6 subject). P3a made the buffer half of that list
# literal with a sha gate over eleven functions in Managers.cpp (scripts/p3a_untouched_regions.sh);
# P4a rewrites the rest of Managers.cpp, DirectGLES.cpp and Utils.cpp by design - the twins become
# handle-shaped, the descriptors replace the frontend reads - so the files' diffs are large and say
# nothing about whether the ring, the permutation, the depth/stencil sampling core or the format
# caveat moved. This gate extracts the seventeen BODIES and compares them on their own.
#
# THE SEVENTEEN. P3a's eleven are carried forward unchanged (BRIEF-P4A.md D-N: "P4a must not touch
# any of the eleven"), so this script is a superset of its parent and the two run side by side -
# the parent keeps answering P3a's question against P3a's base ref, this one answers P4a's.
#
# -- P3a's eleven, in Managers.cpp, in the parent's fixed order --
# 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
# FlushPendingRangesNow the three-tier drain, pull arm
# FlushPendingRangesFrom the SAME three-tier drain, on the arm that ships (ID-15). Born
# in P3a, so its baseline is PINNED, not read from <ref-a>
#
# -- P4a's six (BRIEF-P4A.md D-N), all of which predate the phase --
# StageBlocksIntoUnpackRing Managers.cpp: the unpack-PBO staging repack. It is what makes the
# ring path issue NO glPixelStorei, and the +6 ms/frame Mali cliff
# lives on the other side of it
# UnpackRingAvailable Managers.cpp: honours Features.EsprytDisableUnpackRing and
# self-heals a stale context generation
# UnpackRingAllocate Managers.cpp: on the hot upload route
# RecomputeBackendColorSlots Managers.cpp: the attachment permutation - three passes plus the
# forced ~0 version memo on every moved attachment; removing the
# empty-point detach breaks the invariant (Managers.cpp:7651-7658)
# DepthStencilSamplingReadImpl DirectGLES.cpp: the D24S8 sampling-emulation core (memory
# better-clouds-fullmode), entered from :8617 and :8721
# ShouldUseCaveatTextureFormat Utils.cpp: the format handler P4a's InternalFormat descriptors
# feed; a change here silently changes what every texture is
# allocated as
#
# TWO THINGS THIS SCRIPT ADDS TO ITS PARENT, AND NOTHING ELSE (BRIEF-P4A.md D-N: "keeps every other
# property of its parent verbatim"):
#
# 1. A PER-REGION SOURCE PATH. The parent's SOURCE_PATH is a single file (:85); the seventeen live
# in three. Each row below carries its own path and the extractor reads each file once.
# 2. A REGION KIND. [declared deviation, see DEVIATIONS below] Sixteen of the seventeen are
# FUNCTIONS. `DepthStencilSamplingReadImpl` is a NAMESPACE (DirectGLES.cpp:8117-8578) - the
# brief's D-N table names it as though it were a function, and the parent's extractor, which
# finds a definition as the one `<name> (` whose closing paren is followed by `{`, finds ZERO
# definitions of it and exits 2 forever. Hashing the whole namespace block is also the
# stronger reading of "the D24S8 sampling-emulation CORE": the core is the ~460-line block of
# staging, conversion and readback, not any one function inside it.
#
# Everything else is the parent verbatim, and deliberately so:
#
# 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 a forward declaration and from a call site: a call's `)` is followed by `)`,
# `;` or `,`, never by `{`. A NAMESPACE row is found as the one occurrence of `namespace <name>`
# followed 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 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 this gate could not follow must not read as "nothing moved". Two is the
# expected shape of a #if/#else pair that re-spells one of these bodies beside an untouched copy -
# a finding, not a limitation, because two ladders drift (ID-11, ID-15).
#
# A SHARED TEMPLATE OVER AN ACCESSOR INTERFACE IS REJECTED (ID-13, MEASUREMENTS.md:521): it resizes
# pull symbols and breaks G1. If a P4a arm needs a variant of one of the seventeen it gets its own
# `#if MOBILEGL_PIPE_PUSH` function with its own name and its own pinned sha - the
# FlushPendingRangesFrom shape.
#
# Usage:
# scripts/p4a_untouched_regions.sh <ref-a> <ref-b> compare the seventeen at two git refs
# scripts/p4a_untouched_regions.sh <ref> print the seventeen shas at one ref (a
# baseline capture: ... > p4a-before.sha)
# scripts/p4a_untouched_regions.sh --self-test prove the comparison can go red
#
# stdout is always the sha list - `<sha256> <region>`, one per line, in the fixed order above - so
# a 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 seventeen 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.
#
# DEVIATIONS from BRIEF-P4A.md D-N, both declared here rather than in a commit message so that the
# next reader of this file meets them:
# D-N/1 the namespace region kind, above.
# D-N/2 the PINNED baseline is CONSULTED UNCONDITIONALLY for FlushPendingRangesFrom, where the
# parent consults it only when <ref-a> does not define the function. D-N says that row is
# compared "against its pinned 3e298c9a sha", and at P4a's base ref the function DOES
# exist - so the parent's fallback would silently never fire and the pin would stop being
# the baseline the brief names. Both readings agree on this tree (measured: the body at
# 37da3c3a hashes to the pinned value); where they would ever disagree, this script says
# so on stderr and keeps the PIN, because the pin is the reviewed text.
set -u -o pipefail
# One row per region: <name>@<kind>@<path>. The ORDER is the fixed order the sha list is printed
# in and the order `compare_lists` names the first mover from; P3a's eleven keep their parent's
# positions so a reader can diff the two scripts' outputs.
REGIONS="\
IsPoolable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
EnrollIntoPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
AcquireFromPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
TrimBufferPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
ClearBufferPool@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
ProcessDeferredBufferReleases@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
CreateRingStorage@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
RingAvailable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
RingAllocate@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
FlushPendingRangesNow@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
FlushPendingRangesFrom@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
StageBlocksIntoUnpackRing@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
UnpackRingAvailable@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
UnpackRingAllocate@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
RecomputeBackendColorSlots@function@MobileGL/MG_Backend/DirectGLES/Managers.cpp
DepthStencilSamplingReadImpl@namespace@MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
ShouldUseCaveatTextureFormat@function@MobileGL/MG_Backend/DirectGLES/Utils.cpp"
EXPECTED_FUNCTION_COUNT=17
# The one region born in P3a, so there is no body at P4a's base ref that this phase reviewed: its
# baseline is the sha captured at 3e298c9a, the commit at which the two-arm shape was reviewed and
# accepted (ID-15). See DEVIATIONS D-N/2 for why it is consulted unconditionally.
PINNED_FUNCTIONS="FlushPendingRangesFrom"
PINNED_BASELINE_REF=3e298c9a
PINNED_SHA_FlushPendingRangesFrom=37fc94ffc5991923d222d585daa3af6511d2352d255623026ce35a3b6963c4a6
# The regions the self-test perturbs, one negative control each. FOUR, exactly as D-N requires, and
# each is a different shape so that a control which only ever perturbed the easy one cannot leave
# the others unproven:
# ClearBufferPool P3a's easy control - small, no forward declaration, no overload, so
# a failure there is about the COMPARISON rather than the extraction
# FlushPendingRangesNow P3a's hard control - the longest body in the set, three nested
# tiers, its own early returns, and one of a PAIR of identically
# shaped bodies in two preprocessor arms
# RecomputeBackendColorSlots P4a's method-shaped control: a member function spelled
# `BackendFramebufferObject::RecomputeBackendColorSlots(` with a
# multi-line signature and a call site of its own, so a naive
# extraction picks the wrong occurrence
# StageBlocksIntoUnpackRing P4a's static-in-an-anonymous-namespace control, on the hot upload
# route, and the one whose file position sits between two other
# protected bodies
SELF_TEST_FUNCTIONS="ClearBufferPool FlushPendingRangesNow RecomputeBackendColorSlots StageBlocksIntoUnpackRing"
say() { echo "[p4a-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
# Every distinct source path the rows name, in first-appearance order.
region_paths() {
printf '%s\n' "$REGIONS" | awk -F@ '!seen[$3]++ { print $3 }'
}
# The extractor. Three modes, all over a SPEC FILE of `<name>\t<kind>\t<file>` rows so that the
# self-test can drive it without inventing a commit: `extract` prints one `<sha> <name>` line per
# row in spec order, `perturb` writes a copy of one file with one statement inserted at the top of
# one region's body, `count` is unused by the shell and kept out.
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 == "'" and i > 0 and (text[i - 1].isdigit() or
(text[i - 1] in 'abcdefABCDEF' and i > 1 and
text[i - 2] in "0123456789abcdefABCDEFxX")):
# A C++14 DIGIT SEPARATOR (16'777'216, 0xff'ff), not a char literal. Treating it as one
# would blank forward to the next apostrophe - which can be a whole function away, in a
# comment - and silently swallow a brace, shifting a body's extent with no diagnostic.
i += 1
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_function(text, masked, name):
"""Every (begin, end) at which `name` is DEFINED as a function."""
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 find_namespace(text, masked, name):
"""Every (begin, end) at which `name` is DEFINED as a namespace.
P4a's DepthStencilSamplingReadImpl is a namespace, not a function (DirectGLES.cpp:8117-8578):
the D24S8 sampling-emulation CORE is the whole block, and a function-shaped search finds no
definition of it at all. The closing `} // namespace <name>` comment is masked away by the
time this runs, so the extent is decided by brace matching exactly as a function's is - and a
USE of the namespace (`DepthStencilSamplingReadImpl::Read(...)`) is not matched, because the
`namespace` keyword is not in front of it.
"""
hits = []
for m in re.finditer(r'\bnamespace\s+' + re.escape(name) + r'\b', masked):
rest = masked[m.end():m.end() + 96]
if not rest.lstrip().startswith('{'):
continue
brace = masked.index('{', m.end())
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 find_definition(text, masked, name, kind):
if kind == 'namespace':
return find_namespace(text, masked, name)
return find_function(text, masked, name)
def read_spec(path):
rows = []
with open(path, encoding='utf-8') as spec:
for line in spec:
line = line.rstrip('\n')
if not line:
continue
name, kind, source = line.split('\t')
rows.append((name, kind, source))
return rows
class Sources(object):
"""Each file read and masked once, however many regions name it."""
def __init__(self):
self.cache = {}
def get(self, path):
if path not in self.cache:
text = open(path, encoding='utf-8', newline='').read()
self.cache[path] = (text, mask(text))
return self.cache[path]
def extract(rows):
sources = Sources()
out, problems = [], []
for name, kind, source in rows:
try:
text, masked = sources.get(source)
except OSError as err:
problems.append('%s: cannot read %s (%s)' % (name, source, err))
continue
hits = find_definition(text, masked, name, kind)
if len(hits) != 1:
problems.append('%s: expected exactly one %s definition in %s, found %d'
% (name, kind, source, len(hits)))
if len(hits) > 1:
# The expected shape of this failure, and it is a FINDING rather than a limitation:
# a `#if MOBILEGL_PIPE_PUSH` arm that re-spells one of these bodies beside an
# untouched `#else` copy satisfies G1 (the pull build's text did not move) and
# defeats G5 (the push build compiles a second copy that can drift). The gate
# cannot say which of the two is "the" body, and must not pick one.
for begin, end in hits:
problems.append(' ...definition at line %d, %d lines'
% (text[:begin].count('\n') + 1,
text[:end].count('\n') - text[:begin].count('\n') + 1))
problems.append(' the P4a arm must CALL the untouched %s, not carry a copy of it: '
'two ladders drift (BRIEF-P4A.md D-N, ID-11, ID-15)' % name)
continue
begin, end = hits[0]
body = text[begin:end]
out.append((hashlib.sha256(body.encode('utf-8')).hexdigest(), name))
return out, problems
def perturb(rows, target, src, dst):
for name, kind, source in rows:
if name != target:
continue
text = open(src, encoding='utf-8', newline='').read()
masked = mask(text)
hits = find_definition(text, masked, name, kind)
if len(hits) != 1:
sys.stderr.write('[p4a-untouched] cannot perturb %s: %d definitions\n'
% (target, len(hits)))
return 2
begin, end = hits[0]
# The opening brace is located in the MASKED text and then used as an offset into the
# original: a brace inside a comment or a string on the signature line would otherwise send
# the perturbation somewhere that is not the body, and the control would be proving the
# wrong thing. Offsets are identical between the two by construction (mask() preserves
# length).
brace = masked.index('{', begin)
patched = (text[:brace + 1] +
'\n // p4a_untouched_regions.sh --self-test: a body that MOVED.\n' +
text[brace + 1:])
open(dst, 'w', encoding='utf-8', newline='').write(patched)
return 0
sys.stderr.write('[p4a-untouched] %s is not one of the regions\n' % target)
return 2
def main(argv):
# The sha list is parsed by awk, and on Windows (Git Bash, MSYS python) text-mode stdout
# translates '\n' into CRLF - after which `$2 == n` never matches and the gate exits 1 on an
# untouched tree. Linux CI never saw it; a developer running the gate locally always did.
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(newline='\n')
mode = argv[1]
rows = read_spec(argv[2])
if mode == 'extract':
found, problems = extract(rows)
for problem in problems:
sys.stderr.write('[p4a-untouched] %s\n' % problem)
for sha, name in found:
sys.stdout.write('%s %s\n' % (sha, name))
return 2 if problems else 0
if mode == 'perturb':
return perturb(rows, argv[3], argv[4], argv[5])
sys.stderr.write('[p4a-untouched] unknown mode %r\n' % mode)
return 2
sys.exit(main(sys.argv))
PYTHON
# Write a spec file whose rows point at the blobs in "$2" (a directory holding one file per
# region path, named by a sanitised path so two files of the same basename could never collide).
blob_name() { printf '%s' "$1" | tr '/' '_'; }
write_spec() {
local dir=$1 spec=$2 row name kind source
: > "$spec"
printf '%s\n' "$REGIONS" | while IFS='@' read -r name kind source; do
[ -n "$name" ] || continue
printf '%s\t%s\t%s/%s\n' "$name" "$kind" "$dir" "$(blob_name "$source")" >> "$spec"
done
}
# Materialise every region source at a git ref into "$2".
checkout_ref() {
local ref=$1 dir=$2 source
mkdir -p "$dir" || return 2
for source in $(region_paths); do
if ! git show "$ref:$source" > "$dir/$(blob_name "$source")" 2>"$WORK_DIR/show.err"; then
say "cannot read $source at '$ref':"
sed 's/^/[p4a-untouched] /' "$WORK_DIR/show.err" >&2
return 2
fi
done
return 0
}
# Extract at a git ref into "$WORK_DIR/$2.sha". Every region must be defined exactly once there;
# this is the side the gate is ABOUT (<ref-b>, and the single-ref listing's ref).
extract_ref() {
local ref=$1 out=$2
checkout_ref "$ref" "$WORK_DIR/$out" || return 2
write_spec "$WORK_DIR/$out" "$WORK_DIR/$out.spec"
python3 "$PY" extract "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.sha"
return $?
}
# The BASELINE side (<ref-a>). The sixteen ordinary regions are extracted strictly, so a rename of
# one of THOSE is exit 2 rather than a silently short list. FlushPendingRangesFrom then takes the
# PINNED sha (DEVIATIONS D-N/2) whether or not <ref-a> defines it, and a <ref-a> that defines it
# DIFFERENTLY is reported - loudly - because the two answers disagreeing is itself a finding.
extract_baseline() {
local ref=$1 out=$2 name pinned atRef
checkout_ref "$ref" "$WORK_DIR/$out" || return 2
write_spec "$WORK_DIR/$out" "$WORK_DIR/$out.spec.all"
# The strict pass, minus the pinned rows.
cp -f "$WORK_DIR/$out.spec.all" "$WORK_DIR/$out.spec" || return 2
for name in $PINNED_FUNCTIONS; do
grep -v "^$name$(printf '\t')" "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.spec.tmp" || true
mv -f "$WORK_DIR/$out.spec.tmp" "$WORK_DIR/$out.spec" || return 2
done
if ! python3 "$PY" extract "$WORK_DIR/$out.spec" > "$WORK_DIR/$out.sha" 2>"$WORK_DIR/$out.err"; then
say "the baseline ref '$ref' does not define the sixteen unpinned regions exactly once each:"
sed 's/^/[p4a-untouched] /' "$WORK_DIR/$out.err" >&2
return 2
fi
for name in $PINNED_FUNCTIONS; do
eval "pinned=\$PINNED_SHA_$name"
if [ -z "$pinned" ] || [ "$pinned" = "PLACEHOLDER_SHA" ]; then
say "$name has no pinned baseline sha; that row cannot be compared"
return 2
fi
grep "^$name$(printf '\t')" "$WORK_DIR/$out.spec.all" > "$WORK_DIR/$out.spec.pinned" || true
atRef=$(python3 "$PY" extract "$WORK_DIR/$out.spec.pinned" 2>/dev/null | awk '{ print $1 }')
if [ -n "$atRef" ] && [ "$atRef" != "$pinned" ]; then
say "NOTE: $name IS defined at '$ref' and hashes $atRef, which is NOT the sha pinned in this"
say " script ($pinned, captured at $PINNED_BASELINE_REF). The PIN is what is compared - it is"
say " the reviewed text (ID-15) - but the two disagreeing means the push ladder moved between"
say " $PINNED_BASELINE_REF and '$ref' without this gate being re-pinned. Re-pin deliberately or"
say " revert; do not leave them disagreeing."
fi
printf '%s %s\n' "$pinned" "$name" >> "$WORK_DIR/$out.sha"
done
return 0
}
# Compare two sha lists. Prints the first region that moved, in REGIONS order.
compare_lists() {
local a=$1 b=$2 labelA=$3 labelB=$4 moved=0 name shaA shaB
printf '%s\n' "$REGIONS" | awk -F@ '{ print $1 }' > "$WORK_DIR/order"
while read -r name; do
[ -n "$name" ] || continue
shaA=$(awk -v n="$name" '$2 == n { print $1 }' "$a")
shaB=$(awk -v n="$name" '$2 == n { print $1 }' "$b")
if [ "$shaA" != "$shaB" ]; then
if [ "$moved" -eq 0 ]; then
say "FIRST REGION THAT MOVED: $name"
say " $labelA ${shaA:-<not found>}"
say " $labelB ${shaB:-<not found>}"
say " G5 (ARCHITECTURE.md:318, :321, :515) says the Espryt do-not-touch list is literal:"
say " P3a's buffer pool, deferred-release drain, three rings and BOTH arms of the three-tier"
say " flush drain, plus P4a's unpack-PBO staging repack and its two ring helpers, the"
say " attachment permutation, the D24S8 sampling-emulation core and the format-caveat"
say " handler, all move VERBATIM. If this change is intended it is not a P4a change and it"
say " needs its own commit and its own reason; if it is not, revert the body. A P4a arm that"
say " carries its own COPY of one of these beside an untouched one is the same finding: the"
say " new arm must CALL the untouched region, not re-spell it."
else
say "also moved: $name"
fi
moved=$((moved + 1))
fi
done < "$WORK_DIR/order"
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 ones (an untouched copy
# compares equal; an edit OUTSIDE the regions is invisible) rule out a comparison that reports
# every region as moved, and the four NEGATIVE ones rule out the comparison that never reports any.
if [ "${1:-}" = "--self-test" ]; then
[ $# -eq 1 ] || { say "--self-test takes no other arguments"; exit 2; }
mkdir -p "$WORK_DIR/pristine" || exit 2
for source in $(region_paths); do
[ -f "$source" ] || { say "$source is not in this tree"; exit 2; }
cp -f "$source" "$WORK_DIR/pristine/$(blob_name "$source")" || exit 2
done
write_spec "$WORK_DIR/pristine" "$WORK_DIR/pristine.spec"
if ! python3 "$PY" extract "$WORK_DIR/pristine.spec" > "$WORK_DIR/pristine.sha"; then
say "the extractor could not read the $EXPECTED_FUNCTION_COUNT regions out of the working tree"
exit 2
fi
found=$(wc -l < "$WORK_DIR/pristine.sha")
if [ "$found" -ne "$EXPECTED_FUNCTION_COUNT" ]; then
say "extracted $found regions, expected $EXPECTED_FUNCTION_COUNT"
exit 2
fi
say "positive control: $EXPECTED_FUNCTION_COUNT regions extracted from the working tree"
cp -r "$WORK_DIR/pristine" "$WORK_DIR/copy" || exit 2
write_spec "$WORK_DIR/copy" "$WORK_DIR/copy.spec"
python3 "$PY" extract "$WORK_DIR/copy.spec" > "$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 P4a: the rest of these three
# files IS going to be rewritten (the twins become handle-shaped, the descriptors replace the
# frontend reads), so a gate that fired on any edit to them would have to be switched off in the
# same week it landed. An edit outside the seventeen must be invisible here - in EVERY file, so
# that a per-file extraction bug cannot hide behind the one file that was probed.
cp -r "$WORK_DIR/pristine" "$WORK_DIR/outside" || exit 2
for source in $(region_paths); do
{ echo "// p4a_untouched_regions.sh --self-test: an edit OUTSIDE the seventeen regions."; \
cat "$WORK_DIR/pristine/$(blob_name "$source")"; } > "$WORK_DIR/outside/$(blob_name "$source")"
done
write_spec "$WORK_DIR/outside" "$WORK_DIR/outside.spec"
python3 "$PY" extract "$WORK_DIR/outside.spec" > "$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 seventeen regions was reported as one of them"
say "moving. This gate would fire on every P4a commit to these three files 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 seventeen regions is invisible, in all three files"
# One negative control per SELF_TEST_FUNCTIONS entry. Each is run on its own, from the pristine
# copy, so the message it produces has to NAME that region - a control that only proved "some
# region moved" would not distinguish "this row is compared" from "this row is extracted as an
# empty range and every comparison of it is vacuous".
controls=0
for target in $SELF_TEST_FUNCTIONS; do
targetSource=$(printf '%s\n' "$REGIONS" | awk -F@ -v n="$target" '$1 == n { print $3 }')
[ -n "$targetSource" ] || { say "$target is not one of the regions"; exit 2; }
rm -rf "$WORK_DIR/perturbed"
cp -r "$WORK_DIR/pristine" "$WORK_DIR/perturbed" || exit 2
write_spec "$WORK_DIR/perturbed" "$WORK_DIR/perturbed.spec"
python3 "$PY" perturb "$WORK_DIR/perturbed.spec" "$target" \
"$WORK_DIR/pristine/$(blob_name "$targetSource")" \
"$WORK_DIR/perturbed/$(blob_name "$targetSource")" || exit 2
python3 "$PY" extract "$WORK_DIR/perturbed.spec" > "$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: $target's body was changed and the comparison still"
say "reported every region as identical. This gate cannot go red for the reason it exists, so"
say "every green it has ever printed means nothing."
exit 2
fi
if ! grep -q "FIRST REGION THAT MOVED: $target" "$WORK_DIR/perturbed.err"; then
say "NEGATIVE CONTROL TRIPPED FOR THE WRONG REASON: the comparison went red but did not name"
say "$target as the first region that moved. It said:"
sed 's/^/[p4a-untouched] /' "$WORK_DIR/perturbed.err" >&2
exit 2
fi
controls=$((controls + 1))
say "negative control $controls: a perturbed $target body is reported, and named"
done
if [ "$controls" -ne 4 ]; then
say "expected FOUR negative controls (BRIEF-P4A.md D-N), ran $controls"
exit 2
fi
say "self-test passed: $controls negative controls, all tripped and all named"
exit 0
fi
# --- the gate -------------------------------------------------------------------------------
case $# in
1)
extract_baseline "$1" one || exit 2
cat "$WORK_DIR/one.sha"
say "listed the $EXPECTED_FUNCTION_COUNT regions at $1"
exit 0
;;
2) ;;
*)
say "usage: $0 <ref-a> <ref-b> | $0 <ref> | $0 --self-test"
exit 2
;;
esac
extract_baseline "$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 $EXPECTED_FUNCTION_COUNT pool / ring / unpack-staging / attachment-permutation /"
say "depth-stencil-sampling / format-caveat regions are byte-identical between $1 and $2"
exit 0
fi
exit 1