mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 12:48:32 +09:00
[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:
@@ -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.
|
||||
|
||||
Executable
+345
@@ -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"
|
||||
Executable
+641
@@ -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
|
||||
Reference in New Issue
Block a user