[CI] (.github/workflows, docs/ci, docs/Disaggregated, MG_IntegrationTest, MG_IntegrationTest/Harness, scripts/ci, scripts/data): retire the split barrier and map-push negative controls

- skip census: lane junit-integration-split/control retired with its step
This commit is contained in:
2026-10-18 10:01:56 -04:00
parent 82a8fb816e
commit 81994940c4
13 changed files with 13 additions and 883 deletions
-143
View File
@@ -1,143 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/control_smoke_test.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# A smoke test for the CI negative controls themselves.
#
# A negative control is a gate, and a workflow `run:` block only executes on a runner, so this
# file runs the real control script - the same file .github/workflows/test.yml invokes, not a copy
# of it - against a stubbed ctest, and checks that it passes exactly when it should.
#
# The case that matters is the first one. A stubbed ctest reports a non-empty selection and then
# fails with UNRELATED_CONTROL_FAILURE: a reason that has nothing to do with the knob the control
# turns. The control must report FAILED on it, and PASSED only on its scenarios' own diagnostic.
#
# usage: control_smoke_test.sh
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
WORK="$(mktemp -d)" || exit 1
trap 'rm -rf "${WORK}"' EXIT
STUB_DIR="${WORK}/stub"
mkdir -p "${STUB_DIR}"
cp "${HERE}/testdata/stub_ctest.sh" "${STUB_DIR}/ctest"
chmod +x "${STUB_DIR}/ctest"
passes=0
failures=0
# expect <expected: PASSED|FAILED> <label> -- <command...>
expect() {
want="$1"; label="$2"; shift 3 # shift past the literal "--"
outfile="${WORK}/run.out"
"$@" > "${outfile}" 2>&1
rc=$?
if [ "${rc}" -eq 0 ]; then got="PASSED"; else got="FAILED"; fi
if [ "${got}" = "${want}" ]; then
passes=$((passes + 1))
printf 'ok %-58s %s (rc=%d)\n' "${label}" "${got}" "${rc}"
else
failures=$((failures + 1))
printf 'NOT OK %-58s expected %s, got %s (rc=%d)\n' "${label}" "${want}" "${got}" "${rc}"
sed 's/^/ | /' "${outfile}"
fi
}
# The lane step's own baseline, as the workflow leaves it: the stub's JUnit for the whole label and
# its exit code beside it, in <base>.xml and <base>.rc.
lane_baseline() { # $1 = STUB_MODE, $2 = base path
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" CONTROL_TMPDIR="${WORK}/tmp-$1" \
ctest -L integration-split --no-tests=error --output-junit "$2.xml" > /dev/null 2>&1
echo "$?" > "$2.rc"
}
# run_split <STUB_MODE> [baseline path]: the controls, tallying the lane step's baseline. A path
# with no files behind it is the job whose lane step never ran.
run_split() {
local base="${2:-${WORK}/base-$1}"
[ -n "${2:-}" ] || lane_baseline "$1" "${base}"
env -i PATH="${STUB_DIR}:/usr/bin:/bin" STUB_MODE="$1" \
CTEST=ctest CONTROL_TMPDIR="${WORK}/tmp-$1" SPLIT_BASELINE_JUNIT="${base}.xml" \
bash "${HERE}/split_negative_controls.sh"
}
# run_split_saying <sentence> <args for run_split...>: run_split, and its output must carry the
# sentence that says which baseline it used.
run_split_saying() {
local sentence="$1"
shift
local out
out=$(run_split "$@" 2>&1)
local rc=$?
printf '%s\n' "${out}"
printf '%s' "${out}" | grep -qF "${sentence}" || { echo "missing: ${sentence}"; return 1; }
return "${rc}"
}
# The lane step's JUnit says green but its exit code says red: ctest's "Not Run" is a skip in
# JUnit, so the code is part of the baseline and must be honoured.
run_split_red_code() {
lane_baseline evidence "${WORK}/redcode"
echo 8 > "${WORK}/redcode.rc"
run_split evidence "${WORK}/redcode"
}
# The lane step never ran: the controls rerun the baseline, and the rerun's JUnit stays out of
# CONTROL_TMPDIR/*.xml, which the job uploads for the skip census.
run_split_rerun() {
rm -f "${WORK}/tmp-evidence/isplit.xml"
run_split_saying "reran the DirectGLES.Split. baseline" evidence "${WORK}/absent" || return 1
if [ -e "${WORK}/tmp-evidence/isplit.xml" ]; then
echo "the rerun's JUnit landed in CONTROL_TMPDIR/*.xml"
return 1
fi
}
# The lane step ran nothing of the DirectGLES.Split. arm (only other entries of the label).
run_split_no_arm() {
printf '%s\n' '<testsuite><testcase name="SplitLogPaths.PrivateAndDistinct" status="run"/></testsuite>' \
> "${WORK}/noarm.xml"
echo 0 > "${WORK}/noarm.rc"
run_split evidence "${WORK}/noarm"
}
echo "=== the split lane's E1 / E3(a) controls (scripts/ci/split_negative_controls.sh)"
# THE FINDING, REPRODUCED. Non-empty selection, green baseline, and a red that is not the knob's.
expect FAILED "unrelated failure with a non-empty selection" -- run_split unrelated
# ... and the same control on the same stub, failing for its own reason: it must PASS.
expect PASSED "the scenarios' own diagnostic" -- run_split evidence
# The pre-existing half of the control, which was never broken: a knob that reds nothing.
expect FAILED "the knob leaves the selection green" -- run_split green
# The arming counter's half of the finding: a baseline that is already red cannot arm anything.
expect FAILED "the baseline is already red" -- run_split red-baseline
# P5 is complete: losing the runtime implementation must no longer disarm the gate.
expect FAILED "every split entry skipped (implementation lost)" -- run_split all-skipped
expect FAILED "E1 missing boundary probes" -- run_split e1-empty
expect FAILED "E1 own baseline is already red" -- run_split e1-red-baseline
expect FAILED "E1 restored boundary remains red" -- run_split e1-restore-red
expect FAILED "E1 record never reaches the peer" -- run_split e1-no-peer
expect FAILED "E1 observed no real emission" -- run_split e1-no-emit
expect FAILED "E1 incorrectly waits every record" -- run_split e1-waitall
# The baseline is the lane step's own run; the controls rerun it only when that step never ran.
expect PASSED "the lane step's baseline is tallied, not rerun" -- run_split_saying "no rerun" evidence
expect PASSED "no lane JUnit: the controls rerun the baseline" -- run_split_rerun
expect FAILED "the lane step's exit code is red, its JUnit green" -- run_split_red_code
expect FAILED "the lane JUnit has no DirectGLES.Split. entry" -- run_split_no_arm
echo
echo "smoke test: ${passes} passed, ${failures} failed"
# Keep the private-file cases on the entry point used by CI and the local gate.
if ! bash "${HERE}/testdata/split_private_log_smoke.sh"; then
failures=$((failures + 1))
fi
if [ "${failures}" -gt 0 ]; then
echo "CONTROL_SMOKE_TEST_FAILED"
exit 1
fi
echo "CONTROL_SMOKE_TEST_OK"
+1 -2
View File
@@ -10,8 +10,7 @@
# P5f (f1)'s NEGATIVE CONTROL (P5F-WIRE-COMPLETENESS.md §6): the dual-block rehearsal must be
# the thing the dual-block lane measures.
#
# The contract is split_negative_controls.sh's, shrunk to one control because there is one
# knob:
# Its contract has two rules:
#
# 1. THE BASELINE MUST BE GREEN. DirectGLES.Split.DualBlock.* runs with
# MOBILEGL_IPC_ROLE_SPLIT_STATE=1 exported (the entry deliberately does NOT pin the knob in
-216
View File
@@ -1,216 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/split_negative_controls.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# EXIT GATE E1's NEGATIVE CONTROL and EXIT GATE E3(a)'s.
#
# The body of test.yml's "Negative controls (barrier, map push)" step, which requires the verb
# barrier and the persistent-map push to be load-bearing. It is a file, not an inline `run:` block,
# so scripts/ci/control_smoke_test.sh runs these same lines off a runner against a stubbed ctest.
#
# WHAT THE REVIEW FOUND (ID-46 finding 8, CONFIRMED by execution; ID-48 assigns it here).
# The previous version accepted ANY non-zero ctest exit as "the knob is load-bearing". A timeout, a
# setup abort, an unrelated assertion, a harness that died before it read the knob at all - every
# one of them printed "turned N selected entries red, as it must" and the step went green. The
# verifier demonstrated it: a stubbed ctest with a NON-EMPTY selection that failed with
# `UNRELATED_CONTROL_FAILURE` produced both controls' success messages and HARNESS_EXIT=0.
#
# So each control now has to say WHY the red is its own:
#
# 1. THE BASELINE MUST BE GREEN. The arming run below used to end in `|| true` and count every
# case that was not <skipped/> as "ran" - so a case that RAN AND FAILED armed the controls,
# and a control that turns an already-red entry red proves nothing at all. It now counts
# PASSED cases, and a baseline with any failure in it is a hard error rather than an arming
# signal.
# 2. THE RED MUST CARRY THE SELECTED CASE'S OWN FAILURE TEXT. Each control names a regex of the
# diagnostics its scenarios emit when that knob is off, and the red is refused if the output
# carries none of them.
#
# WHY THE EVIDENCE IS THE SCENARIO'S ASSERTION TEXT AND NOT THE KNOB'S OWN LOG LINE.
# ConfigLoader logs a named line for both knobs ("is the R-1 NEGATIVE
# CONTROL", "is the E3(a) NEGATIVE CONTROL"), and it is tempting to grep for that. It is not
# evidence: it is written at config load, by every process in the run, whatever happens next. A
# setup abort would carry it too. It proves the knob was READ, never that the knob caused the red.
# Only the failing case's own diagnostic does that. E1 now observes deterministic transport
# waits in dedicated CPU tests; E3(a) retains its scenario assertion and private library log.
# P5e ID-122: ordinary run-ahead draws need not fail with VERB_BARRIER=0, and the old
# overlap Fatal cannot fire with the default BATCH_WAITS=1. Pixel failures are not E1 evidence.
#
# Usage: split_negative_controls.sh [--self-test]
# CTEST ctest binary (default: ctest)
# CONTROL_TMPDIR scratch dir for the junit + output (default: ${RUNNER_TEMP:-/tmp})
# SPLIT_BASELINE_JUNIT the JUnit an earlier `ctest -L integration-split` wrote; its exit code is
# read from the file of the same name ending in .rc. When both exist the
# baseline is tallied from them instead of re-run.
set -u
if [ "${1:-}" = "--self-test" ]; then
bash "$(dirname "$0")/control_smoke_test.sh"
exit $?
fi
CTEST="${CTEST:-ctest}"
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
mkdir -p "${CONTROL_TMPDIR}"
log_helper="$(dirname "$0")/../../MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
# Check ownership even while the runtime lane is disarmed and will skip.
python3 "${log_helper}" check "${CTEST}" "$PWD" || exit 1
# ---- the baseline ---------------------------------------------------------------------------
#
# THE ARMED STATE IS DERIVED FROM BEHAVIOUR, not from a marker string in the generated ctest files.
# The first version read MGITEST_REMOTE_CLIENT_PRESENT out of *_tests.cmake, which was a
# restatement of the CMake source probe review finding M-1 falsified; the arming condition is a
# runtime fact inside each test process (MG_Config::Transport, ClientSession::Active(),
# ImplementedVerbCount(), read by Harness/SplitRuntimePeek), so the only honest way to ask it from a
# shell is to look at what the entries DID.
# The metadata ownership test shares the label but cannot arm runtime controls.
#
# The lane step before this one already ran every integration-split entry on this runner and
# library, and left its JUnit and exit code. Both are read when they exist; only the
# DirectGLES.Split. entries count, as in the rerun. The rerun stays for a job whose lane step
# never ran (an earlier gate failed), where this baseline is the only run of those entries.
baseline_rc_file="${SPLIT_BASELINE_JUNIT:-}"
baseline_rc_file="${baseline_rc_file%.xml}.rc"
if [ -n "${SPLIT_BASELINE_JUNIT:-}" ] && [ -f "${SPLIT_BASELINE_JUNIT}" ] && [ -f "${baseline_rc_file}" ]; then
junit="${SPLIT_BASELINE_JUNIT}"
baseline_rc=$(tr -dc '0-9' < "${baseline_rc_file}")
baseline_rc="${baseline_rc:-1}"
echo "baseline tallied from ${junit} (the lane step's own run, ctest exit ${baseline_rc}); no rerun"
else
# A directory of its own: the job uploads CONTROL_TMPDIR/*.xml for the skip census, which has
# no row for this rerun.
mkdir -p "${CONTROL_TMPDIR}/split-controls"
junit="${CONTROL_TMPDIR}/split-controls/isplit.xml"
rm -f "${junit}"
echo "::notice::no baseline JUnit and exit code from the lane step (SPLIT_BASELINE_JUNIT='${SPLIT_BASELINE_JUNIT:-}'), so the controls reran the DirectGLES.Split. baseline"
"${CTEST}" -L integration-split -R '^DirectGLES\.Split\.' -j 4 --no-tests=error --output-junit "${junit}"
baseline_rc=$?
fi
if [ ! -f "${junit}" ]; then
echo "::error::the baseline run wrote no ${junit} (ctest exit ${baseline_rc}), so nothing below can tell an armed lane from a broken one"
exit 1
fi
tally=$(python3 "$(dirname "$0")/junit_tally.py" "${junit}" --require-split-ran)
if [ -z "${tally}" ]; then
echo "::error::could not tally ${junit}; a run whose result cannot be read is not an arming signal"
exit 1
fi
baseline_passed=$(echo "${tally}" | cut -d' ' -f1)
baseline_failed=$(echo "${tally}" | cut -d' ' -f2)
baseline_skipped=$(echo "${tally}" | cut -d' ' -f3)
echo "split entries - passed: ${baseline_passed}, failed: ${baseline_failed}, skipped: ${baseline_skipped} (ctest exit ${baseline_rc})"
# A RED BASELINE DISARMS THE CONTROLS RATHER THAN ARMING THEM (review finding 8, second half).
# `|| true` plus a "not skipped" counter used to treat a case that ran and FAILED as evidence the
# lane was live. Turning an already-red entry red is not a measurement.
if [ "${baseline_failed}" -gt 0 ] || [ "${baseline_rc}" -ne 0 ]; then
echo "::error::${baseline_failed} DirectGLES.Split. entries are ALREADY RED with both knobs at their defaults, so neither negative control below can attribute its red to the knob it turns. Fix the lane first; a control measured against a red baseline is not a control. (This used to be swallowed by an unconditional '|| true' and counted as 'the lane is armed'.)"
exit 1
fi
if [ "${baseline_passed}" -lt 1 ]; then
echo "::error::split baseline FAILED: every DirectGLES.Split. entry SKIPPED; the split implementation did not execute"
exit 1
fi
# ---- the controls ---------------------------------------------------------------------------
#
# run_control <name> <filter> <ctest evidence regex> <private-log evidence regex|""> <VAR=VALUE>...
#
# THE FOURTH ARGUMENT IS NEW AND E3(a) IS WHY. The header above argues that the knob's own
# ConfigLoader line is not evidence, and that is still true: it is written at config load, by
# every process in the run, whatever happens next. What IS evidence is a line the knob's
# BEHAVIOUR emits at the site that changed - and until now E3(a) had none, because
# PersistentMapTracker::PushBlocksFor simply `return`ed at blockBytes == 0. The joint gate
# recorded exactly that ("There is no Fatal for block size zero ... No nonexistent private-file
# Fatal is quoted", joint-v1.md 3) and ID-65 assigned the missing line here. With it, E3(a) no
# longer rests on a pixel assertion alone: the red must carry the scenario's own diagnostic AND
# the library's own statement that the push was disabled, from the entry's private file.
# The optional 5th positional is a ctest -E EXCLUSION, applied to every ctest invocation this
# function makes. It exists because ctest -R is POSIX ERE: there is no negative lookahead, so a
# selection that must say "these, except that one" cannot say it in -R alone. E1 below is the
# case that forced it (ID-122).
run_control() {
name="$1"; filter="$2"; evidence="$3"; private_evidence="$4"; exclude="${5:-}"; shift 5
if [ -n "${exclude}" ]; then EX=(-E "${exclude}"); else EX=(); fi
export SPLIT_LOG_EXCLUDE="${exclude}"
matched=$("${CTEST}" -N -L integration-split -R "${filter}" "${EX[@]}" | grep -cE '^ *Test *#[0-9]+:')
if [ "${matched}" -lt 1 ]; then
echo "::error::${name} selected ${matched} tests; its filter no longer matches anything"
exit 1
fi
manifest="${CONTROL_TMPDIR}/split-tests.json"
"${CTEST}" --show-only=json-v1 > "${manifest}" || exit 1
# Remove selected files first: a previous Fatal must never arm a new red.
python3 "${log_helper}" reset "${manifest}" "${filter}" || exit 1
out="${CONTROL_TMPDIR}/control-output.txt"
result="${CONTROL_TMPDIR}/control.xml"
rm -f "${result}"
env "$@" "${CTEST}" --output-on-failure -L integration-split -R "${filter}" "${EX[@]}" --no-tests=error --output-junit "${result}" > "${out}" 2>&1
control_rc=$?
cat "${out}"
# Inspect JUnit before exit status or private Fatal: a skipped pre-flight can carry both.
python3 "${log_helper}" results "${manifest}" "${filter}" "${result}" 'E3(a)' || exit 1
if [ "${control_rc}" -eq 0 ]; then
echo "::error::${name} left ${matched} split entries GREEN, so the knob it turns is not load-bearing and the gate it controls proves nothing."
exit 1
fi
if ! python3 "${log_helper}" assertion "${manifest}" "${filter}" "${result}" "${evidence}"; then
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
exit 1
fi
# ... and, where the knob has one, the LIBRARY's own line as well, out of the entry's private
# file. Both halves are required: the scenario assertion says the pixels were wrong, and this
# says the code path the knob turns off is the one that stopped running. A red that has only
# the first half is consistent with any other defect in the same scenario.
if [ -n "${private_evidence}" ]; then
python3 "${log_helper}" evidence "${manifest}" "${filter}" "${private_evidence}" "${name}" || exit 1
fi
echo "${name} turned ${matched} selected entries red, and the red carries the scenario's own diagnostic, as it must"
}
# E1: the default arm must actually park for a kWaitApplied record and for a
# kWaitNone record whose server has no run-ahead cap. Removing the verb barrier
# must fail those exact return-before-apply assertions. A third positive case
# proves that a cap-authorized kWaitNone record does not wait. The peer releases
# on an observed Park or on the client's return, not on a sleep or pixel race.
# The helper demands green baseline, two named reds, and green restoration;
# skipped/missing cases, timeouts and unrelated failures never count as evidence.
python3 "$(dirname "$0")/wait_boundary_negative_control.py" \
--ctest "${CTEST}" --out "${CONTROL_TMPDIR}/e1" || exit 1
# E3(a): PersistentMapTracker::PushBlocksFor stops at blockBytes == 0 - deliberately, because 0
# is the negative control and not "unlimited". Two independent halves are now required:
# * the SCENARIO's own assertion in ctest's output. PersistentCoherentMapScenario.cpp:414-417 /
# 442-443 name the missing second write; a generic source-line Failure is not accepted,
# because an unrelated assertion in the same case is not this red;
# * the LIBRARY's own line in the entry's private file, saying the push was disabled by this
# knob. It did not exist until ID-65 assigned it (joint-v1.md 3), which is why this control
# used to rest on the pixels alone.
# TheMapLandsInTheArmItsLaneDeclares skips by design outside PersistentMapArm.
# Select only the pixel cases; a pre-flight skip in either remains a hard failure.
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
'DirectGLES\.Split\.(SmallRing\.)?PersistentCoherentMapScenario\.(TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw|AWriteAfterAFrameBoundaryReachesTheNextFramesDraw)$' \
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping, after a Present" \
'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0' \
'' \
MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
-47
View File
@@ -1,47 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/testdata/split_private_log_smoke.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# Exercise E1's per-test boundary evidence and E3(a)'s private-file read (R-16).
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
WORK=$(mktemp -d "${TMPDIR:-/tmp}/split-private-log.XXXXXX")
trap 'rm -rf "${WORK}"' EXIT
cp "${HERE}/testdata/stub_ctest.sh" "${WORK}/ctest"
chmod +x "${WORK}/ctest"
passes=0
for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-private skipped-selection e3-skipped-selection notrun-selection missing-selection partial-fatal wrong-fatal; do
mkdir -p "${WORK}/${mode}"
rc=0
STUB_MODE="${mode}" CTEST="${WORK}/ctest" CONTROL_TMPDIR="${WORK}/${mode}" \
bash "${HERE}/split_negative_controls.sh" > "${WORK}/${mode}.out" 2>&1 || rc=$?
if [ "${mode}" = evidence ]; then
[ "${rc}" = 0 ] && grep -q "negative control E3(a).*scenario's own diagnostic" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK own boundary/private-file evidence must PASS"; exit 1;
}
grep -qFx "private-log evidence: DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw: ${WORK}/${mode}/pmap.log" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK private-file evidence line must name the selected entry and path"; exit 1;
}
else
message='red lacks its own observed wait-boundary failure'
[ "${mode}" != e3-unrelated ] || message='FAILED: red lacks its persistent-map push diagnostic'
case "${mode}" in
e3-no-private) message='no selected private log carries /MGPipe: persistent-map push disabled' ;;
skipped-selection|notrun-selection) message='selected testcase skipped or did not run' ;;
e3-skipped-selection) message='SplitLogPaths FAILED: E3(a) control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
missing-selection) message='selected testcase names are missing, duplicated or unexpected' ;;
esac
match=(-qF)
[ "${rc}" != 0 ] && grep "${match[@]}" "${message}" "${WORK}/${mode}.out" || {
cat "${WORK}/${mode}.out"; echo "NOT OK ${mode}: control must report FAILED for its own reason"; exit 1;
}
fi
echo "ok private-log ${mode} (control rc=${rc})"
passes=$((passes + 1))
done
echo "private-log smoke test: ${passes} passed, 0 failed"
-269
View File
@@ -1,269 +0,0 @@
#!/bin/bash
# MobileGL - scripts/ci/testdata/stub_ctest.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# A stubbed `ctest` for scripts/ci/control_smoke_test.sh.
#
# Descended from the verification agent's stub (wave1-codex-verify.md 8, ~/w7/p5-verify-f8-stub/ctest),
# which is what CONFIRMED that the negative controls accepted an unrelated failure. Every mode below
# is deliberately the BEST case for the control under test: the selection is never empty except in
# the mode that exists to test the empty-selection guard, and the baseline is green except in the
# mode that exists to test the red-baseline guard. If a control passes here it is because the
# control's logic is wrong, not because the stub starved it.
#
# STUB_MODE:
# unrelated baseline green; the control's own run fails with UNRELATED_CONTROL_FAILURE
# evidence baseline green; the control's own run fails with the scenarios' own wording
# missing-fatal private file exists but lacks Fatal (stdout status still fails)
# stdout-fatal Fatal exists only on stdout, never in the private file
# stale-fatal Fatal exists before reset, never from this control run
# e3-unrelated E1 has its private Fatal; E3(a) fails for an unrelated reason
# skipped-selection knob kills pre-flight; private Fatal exists, entry skips, ctest exits 0
# notrun-selection selected entry was not run, despite a private Fatal
# missing-selection selected entry is absent from the result XML
# partial-fatal two entries fail, but only the first has its expected private Fatal
# wrong-fatal entry fails with a different Fatal in its private file
# e3-no-private E1 has its private Fatal; E3(a) has its PIXEL assertion but the library
# never says the push was disabled (the half ID-65 added)
# green baseline green; the control's own run PASSES (the knob is not load-bearing)
# red-baseline the baseline itself has a failed entry
# all-skipped the baseline is entirely skipped (lost implementation, a hard failure)
set -u
mode="${STUB_MODE:?STUB_MODE must be set}"
listing=1
junit=""
prev=""
for a in "$@"; do
[ "$a" = "--show-only=json-v1" ] && json_requested=1
[ "$a" = "-N" ] && listing_requested=1
if [ "$prev" = "--output-junit" ]; then junit="$a"; fi
if [ "$prev" = "-R" ]; then selector="$a"; fi
prev="$a"
done
listing_requested="${listing_requested:-0}"
# E1's CPU-only boundary probes use their own testcase results. Keep observations
# inside each testcase so stdout-only and stale evidence remain rejected.
if [[ "${selector:-}" == *RemoteWaitBoundaryControl* ]]; then
python3 - "${mode}" "${junit}" "${selector}" <<'PY'
import os, re, sys
import xml.etree.ElementTree as ET
mode, path, selector = sys.argv[1:]
barrier = int(os.environ['MOBILEGL_IPC_VERB_BARRIER'])
negative = barrier == 0
prefix = 'RemoteWaitBoundaryControl.'
specs = [
('AppliedClassWaitsWithRunAheadCap', 'ApplierReset', 1, True,
'E1 wait boundary: ApplierReset returned before apply'),
('MissingServerCapKeepsClearLockstep', 'Clear', 0, True,
'E1 wait boundary: Clear without server cap returned before apply'),
('ServerCapAllowsClearToRunAhead', 'Clear', 1, False, ''),
]
root = ET.Element('testsuite')
failures = 0
for index, (short, op, cap, waits, diagnostic) in enumerate(specs):
name = prefix + short
if not re.search(selector, name):
continue
if mode == 'e1-empty' or (negative and mode == 'missing-selection' and index == 1):
continue
case = ET.SubElement(root, 'testcase', name=name, status='run')
waited = int(waits and barrier == 1)
observation = (f'E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} '
f'applied_before_return={waited} emitted=1 peer_seen=1')
failed = negative and mode != 'green'
if mode == 'e1-red-baseline' and 'baseline' in path:
failed = True
if mode == 'e1-restore-red' and 'restored' in path:
failed = True
if mode == 'e1-waitall' and not waits:
failed = True
observation = observation.replace('parked=0', 'parked=1').replace('applied_before_return=0', 'applied_before_return=1')
if negative and mode == 'skipped-selection':
ET.SubElement(case, 'skipped')
failed = False
if negative and mode == 'notrun-selection':
case.set('status', 'notrun')
failed = False
if failed:
ET.SubElement(case, 'failure', message='control red')
failures += 1
text = observation + '\n' + (diagnostic if negative else '')
if negative:
if mode == 'unrelated' or mode == 'wrong-fatal':
text = observation + '\nUNRELATED_CONTROL_FAILURE'
elif mode == 'missing-fatal' or (mode == 'partial-fatal' and index == 1):
text = diagnostic
elif mode == 'stdout-fatal':
print(text) # Must not qualify as this testcase's own evidence.
text = 'UNRELATED_CONTROL_FAILURE'
elif mode == 'stale-fatal':
text = text.replace('barrier=0', 'barrier=1')
elif mode == 'e1-no-peer':
text = text.replace('peer_seen=1', 'peer_seen=0')
elif mode == 'e1-no-emit':
text = text.replace('emitted=1', 'emitted=0')
ET.SubElement(case, 'system-out').text = text
ET.ElementTree(root).write(path, encoding='utf-8', xml_declaration=True)
print(f'E1 stub: {len(root)} selected, {failures} failed')
sys.exit(8 if failures else 0)
PY
exit $?
fi
emit_listing() {
echo "Test project /stub"
echo " Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels"
if [ "${mode}" = partial-fatal ]; then
echo " Test #2: DirectGLES.Split.TriangleScenario.SecondEntry"
echo "Total Tests: 2"
else
echo "Total Tests: 1"
fi
}
write_junit() {
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ] || [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
entry=DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels
[ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ] || entry=DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw
body="<testcase name=\"${entry}\" status=\"fail\"><failure message=\"control red\"/></testcase>"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
case "${mode}" in
evidence|e3-no-private)
body="<testcase name=\"${entry}\" status=\"fail\"><failure/><system-out>the SECOND write through the same mapping, announced by nothing</system-out></testcase>" ;;
esac
fi
if [ "${mode}" = partial-fatal ] && [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
body="${body}<testcase name=\"DirectGLES.Split.TriangleScenario.SecondEntry\" status=\"fail\"><failure/></testcase>"
fi
case "${mode}" in
e3-skipped-selection)
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>"
fi ;;
skipped-selection) body="<testcase name=\"${entry}\" status=\"notrun\"><skipped/></testcase>" ;;
notrun-selection) body="<testcase name=\"${entry}\" status=\"notrun\"/>" ;;
missing-selection) body='' ;;
green) body="<testcase name=\"${entry}\" status=\"run\"/>" ;;
esac
printf '%s\n' "<testsuite>${body}</testsuite>" > "$1"
return
fi
case "${mode}" in
red-baseline)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="failed"><failure message="already red"/></testcase>'
;;
all-skipped)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="notrun"><skipped/></testcase>'
;;
*)
body='<testcase name="DirectGLES.Split.TriangleScenario.AVboBackedTriangleReachesReadPixels" status="run" time="0.3"/>'
;;
esac
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' "<testsuite name=\"stub\">" " ${body}" '</testsuite>' > "$1"
}
# Model the library file sink separately from ctest stdout (ID-53).
#
# P6: the real sink writes one file per ROLE, `<base>.client.log` / `<base>.server.log`, and the
# validator derives those from the ENV base. So the manifest below keeps the base names (what the
# lane sets) while the on-disk writes here land in the CLIENT file - which is the role that raises
# these markers in a real run.
log="${CONTROL_TMPDIR}/entry.client.log"
if [ "${json_requested:-0}" = 1 ]; then
python3 -c 'import json, os; p=os.environ["CONTROL_TMPDIR"]; entries=[("ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels", "entry.log"), ("PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw", "pmap.log")]; entries += [("TriangleScenario.SecondEntry", "second.log")] if os.environ["STUB_MODE"] == "partial-fatal" else []; print(json.dumps({"tests": [{"name": "DirectGLES.Split."+n, "properties": [{"name": "LABELS", "value": ["integration-split"]}, {"name": "ENVIRONMENT", "value": ["MOBILEGL_LOG_FILE_PATH="+p+"/"+f]}]} for n,f in entries]}))'
if [ "${mode}" = stale-fatal ]; then
echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}"
fi
exit 0
fi
if [ "${listing_requested}" = "1" ]; then
emit_listing
exit 0
fi
if [ -n "${junit}" ]; then
write_junit "${junit}"
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" != 0 ] && [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" != 0 ]; then
case "${mode}" in
red-baseline) echo "1/1 Test #1: ... ***Failed"; exit 8 ;;
*) echo "100% tests passed, 0 tests failed out of 1"; exit 0 ;;
esac
fi
fi
# The control's own run.
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
case "${mode}" in
evidence|e3-unrelated|e3-no-private|skipped-selection|e3-skipped-selection|notrun-selection|missing-selection|partial-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}" ;;
wrong-fatal) echo 'Fatal{ReplyMissing, "DrawVbo"}' > "${log}" ;;
missing-fatal) echo "library setup only; no fatal" > "${log}" ;;
stdout-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' ;;
esac
fi
# E3(a)'s library line, in the PERSISTENT-MAP entry's own private file - a different file from
# E1's, exactly as the manifest above declares. `e3-no-private` is the mode that leaves it out:
# the pixel assertion arrives, the library says nothing, and the control must refuse the red.
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ] && [ "${mode}" = evidence ]; then
echo 'MGPipe: persistent-map push disabled - MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is exit gate E3(a)'"'"'s NEGATIVE CONTROL' \
> "${CONTROL_TMPDIR}/pmap.client.log"
fi
case "${mode}" in
e3-skipped-selection)
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
echo 'selected E3 entry ... ***Skipped'
exit 0
fi
exit 8
;;
skipped-selection|notrun-selection|missing-selection)
echo '1/1 Test #1: selected entry ... ***Skipped'
echo '100% tests passed, 0 tests failed out of 1'
exit 0
;;
e3-no-private)
# E1's half passes (its private Fatal is written above); E3(a)'s pixel assertion arrives on
# stdout and its private line does not, so the control must stop at the second half.
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
echo "../MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp:414: Failure"
echo "the SECOND write through the same mapping, announced by nothing: this is exit gate E3(b)"
fi
exit 8
;;
unrelated|missing-fatal|stdout-fatal|stale-fatal|e3-unrelated|partial-fatal|wrong-fatal)
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
echo "UNRELATED_CONTROL_FAILURE: the harness aborted in setup before the knob was read"
if [ "${MOBILEGL_IPC_PERSISTENT_BLOCK_KB:-64}" = 0 ]; then
case "${mode}" in
missing-fatal|stdout-fatal|stale-fatal)
echo "the SECOND write through the same mapping, announced by nothing" ;;
esac
fi
exit 8
;;
evidence)
# E1 is file-only above; E3(a) emits its scenario assertion to ctest.
echo "1/1 Test #1: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels ...***Failed"
echo "../MobileGL/MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp:290: Failure"
echo "Expected: (bottom.r) > (200), actual: '\\0' vs 200"
echo "the SECOND write through the same mapping, announced by nothing: this is exit gate E3(b)"
exit 8
;;
green)
echo "100% tests passed, 0 tests failed out of 1"
exit 0
;;
*)
echo "stub_ctest: unknown STUB_MODE '${mode}'" >&2
exit 127
;;
esac
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
# MobileGL - scripts/ci/wait_boundary_negative_control.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""E1: green/red/restored-green proof of real ClientSession wait boundaries.
The peer holds apply until the actual transport parks or the emit call returns.
VERB_BARRIER=0 must fail only the two owed waits; ordinary run-ahead draws need
not fail. Each arm is checked from its own CTest JUnit testcase, never a global
grep, stale library log, arbitrary nonzero exit, or timing-dependent pixel.
"""
import argparse
import os
from pathlib import Path
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
PREFIX = "RemoteWaitBoundaryControl."
CASES = {
"AppliedClassWaitsWithRunAheadCap": ("ApplierReset", 1, True,
"E1 wait boundary: ApplierReset returned before apply"),
"MissingServerCapKeepsClearLockstep": ("Clear", 0, True,
"E1 wait boundary: Clear without server cap returned before apply"),
"ServerCapAllowsClearToRunAhead": ("Clear", 1, False, ""),
}
def own_negative(text, expected_observation, diagnostic):
return expected_observation in text and diagnostic in text
def inspect(path, selected, barrier, negative, rc):
root = ET.parse(path).getroot()
cases = root.findall(".//testcase")
names = [case.get("name") for case in cases]
if len(names) != len(set(names)) or set(names) != set(selected):
raise ValueError("selected testcase names are missing, duplicated or unexpected")
for case in cases:
name = case.get("name")
if case.find("skipped") is not None or case.get("status") in {"notrun", "disabled"}:
raise ValueError(f"{name}: selected testcase skipped or did not run")
failed = case.find("failure") is not None or case.find("error") is not None
op, cap, waits, diagnostic = CASES[name.removeprefix(PREFIX)]
waited = int(waits and barrier == 1)
observation = (f"E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} "
f"applied_before_return={waited} emitted=1 peer_seen=1")
text = "\n".join(case.itertext())
if negative:
if not failed or rc == 0:
raise ValueError(f"{name}: disabled barrier did not fail the owed wait")
if not own_negative(text, observation, diagnostic):
raise ValueError(f"{name}: red lacks its own observed wait-boundary failure")
elif failed or rc != 0 or observation not in text:
raise ValueError(f"{name}: baseline/restored arm did not execute its green boundary proof")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
all_cases = [PREFIX + name for name in CASES]
negative_cases = [PREFIX + name for name, (_, _, waits, _) in CASES.items() if waits]
ok = True
for arm, barrier, negative, selected in (
("baseline", 1, False, all_cases),
("control", 0, True, negative_cases),
("restored", 1, False, all_cases),
):
# Never run a negative arm against a failed baseline. A failed negative
# still proceeds through restoration and must not consume stale XML.
if arm == "control" and not ok:
continue
result = args.out / f"e1-{arm}.xml"
result.unlink(missing_ok=True)
env = os.environ.copy()
env["MOBILEGL_IPC_VERB_BARRIER"] = str(barrier)
selector = "^(" + "|".join(re.escape(name) for name in selected) + ")$"
command = [args.ctest, "-L", "unit", "-R", selector, "--no-tests=error",
"--output-on-failure", "--output-junit", str(result)]
try:
run = subprocess.run(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, timeout=90)
(args.out / f"e1-{arm}.out").write_text(run.stdout, encoding="utf-8")
print(run.stdout, end="", flush=True)
inspect(result, selected, barrier, negative, run.returncode)
print(f"E1 {arm}: {'EXPECTED_RED' if negative else 'GREEN'} ({len(selected)} cases)", flush=True)
except (OSError, ValueError, ET.ParseError, subprocess.TimeoutExpired) as exc:
print(f"E1 FAILED: {arm}: {exc}", file=sys.stderr, flush=True)
ok = False
if ok:
print("negative control E1: owed waits failed by their own observed boundary assertions; baseline and restoration green")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main())
-9
View File
@@ -12983,15 +12983,6 @@
"names_from": "junit-integration-monolith-control-monolith/integration",
"optional": "runs only when build-linux-split builds its own library, or the split lane's logs do not prove the IPC defaults"
},
"junit-integration-split/control": {
"skipped": [],
"names": [
"DirectGLES.Split.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"DirectGLES.Split.SmallRing.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw"
]
},
"junit-integration-split/spawn-baseline": {
"skipped": [
"DirectGLES.Spawn.ClipDistanceScenario.ADisabledClipDistanceRemovesNothing",