[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
+4 -27
View File
@@ -1689,11 +1689,7 @@ jobs:
ulimit -c unlimited
export MOBILEGL_FLATC_EXECUTABLE="${RUNNER_TEMP}/flatc-build/flatc"
test -x "${MOBILEGL_FLATC_EXECUTABLE}"
# The exit code is kept beside the JUnit: the controls step tallies this run as its baseline.
rc=0
ctest --output-on-failure -L integration-split --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/split-baseline.xml" || rc=$?
echo "${rc}" > "${RUNNER_TEMP}/split-baseline.rc"
[ "${rc}" -eq 0 ] || exit "${rc}"
ctest --output-on-failure -L integration-split --no-tests=error -j 4 --output-junit "${RUNNER_TEMP}/split-baseline.xml"
python3 ../scripts/ci/junit_tally.py "${RUNNER_TEMP}/split-baseline.xml" --require-split-ran
# Exit gate 9.3: the spawn arm green at the same count. It runs the same cases through a
# SECOND PROCESS, so a failure here that the line above did not see is a transport fact
@@ -1782,21 +1778,6 @@ jobs:
print(f"the two arms agree on all {len(a)} entries, name and status")
PY
# Negative controls for the verb barrier (MOBILEGL_IPC_VERB_BARRIER=0) and the persistent-map
# push (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0): each knob must turn its selected entries red.
# The body is scripts/ci/split_negative_controls.sh, so scripts/ci/control_smoke_test.sh can
# run the same lines off a runner. It needs a green baseline with at least one passed entry,
# tallied from the lane step's JUnit and exit code (re-run only when that step never ran),
# and accepts a red only when it carries the control's own diagnostic.
- name: Negative controls (barrier, map push)
if: ${{ !cancelled() }}
working-directory: ${{ env.SPLIT_BUILD_DIR }}
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
CONTROL_TMPDIR: ${{ runner.temp }}
SPLIT_BASELINE_JUNIT: ${{ runner.temp }}/split-baseline.xml
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
# build-linux ran the server link-closure ratchet on the build these lanes took, with
# continue-on-error so that a red there does not skip build-linux's own consumers.
- name: Server link-closure ratchet of the reused build
@@ -3869,15 +3850,11 @@ jobs:
python3 scripts/gen_pipe_field_ownership.py --check
python3 scripts/gen_pipe_field_ownership.py --self-test
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2).
# control_smoke_test.sh runs the split control script against a stubbed ctest: an unrelated
# failure or an empty selection must make it report FAILED, and the scenarios' real
# diagnostics must make it report PASSED. The Python tests run the trace and TCP lane tooling
# on stubs and synthetic data; test_run_trace_case.py feeds run_trace_case.cmake canned logs.
# No branch guard; no build, GPU or device.
# Negative controls must themselves be able to go red (ARCHITECTURE.md §13.2). The Python
# tests run the trace and TCP lane tooling on stubs and synthetic data; test_run_trace_case.py
# feeds run_trace_case.cmake canned logs. No branch guard; no build, GPU or device.
- name: Control and trace-tool smoke tests (R-16)
run: |
bash scripts/ci/control_smoke_test.sh
python3 scripts/ci/test_trace_infrastructure.py
python3 scripts/ci/tcp_lane_tools_test.py
python3 tools/trace_replay/test_run_tcp_matrix.py
+1 -1
View File
@@ -2410,7 +2410,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# running it here captured only the calls made ABOVE this line and silently dropped the F1
# and Ct blocks below it. SplitLogPaths.PrivateAndDistinct is what caught that, naming
# F1WireScenario.ClearBufferfiPixels; the generated file now sees every call.
# Python is mandatory for this split integration gate (also used by the CI controls).
# Python is mandatory for this split integration gate.
# Do not silently omit the ownership check when the interpreter is unavailable.
find_package(Python3 REQUIRED COMPONENTS Interpreter)
add_test(NAME SplitLogPaths.PrivateAndDistinct
@@ -7,8 +7,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Validate discovered Split log ownership; read only freshly reset control logs."""
import os
"""Validate discovered Split log ownership and read the entries' private logs."""
import json
from pathlib import Path
import re
@@ -699,66 +698,7 @@ def main():
f"{' '.join(f'{k}={v}' for k, v in expected.items())}: "
f"{['proven', '', '', 'a default differs'][code] if code != 1 else 'FAILED'}")
sys.exit(code)
selected = paths(json.loads(Path(sys.argv[2]).read_text()))
selected = {name: path for name, path in selected.items() if re.search(sys.argv[3], name)}
# The SAME exclusion run_control hands ctest -E. Without it this helper and ctest disagree
# about the subject set, and the helper reports an entry ctest never ran as "did not run"
# (P5e, ID-122). ctest -R is POSIX ERE and cannot express the exclusion inline.
_excl = os.environ.get("SPLIT_LOG_EXCLUDE", "")
if _excl:
selected = {n: q for n, q in selected.items() if not re.search(_excl, n)}
if not selected:
raise ValueError(f"integration-split: empty selection for {sys.argv[3]}")
if mode == "reset":
for path in selected.values():
Path(path).unlink(missing_ok=True)
elif mode == "results":
cases = ET.parse(sys.argv[4]).getroot().findall(".//testcase")
label = sys.argv[5]
by_name = {}
for case in cases:
by_name.setdefault(case.get("name"), []).append(case)
skipped = sum(any(c.find("skipped") is not None for c in by_name.get(n, []))
for n in selected)
# ID-62: pre-flight Fatal is not evidence that a selected entry ran.
if skipped:
raise ValueError(f"{label} control: the knob killed the pre-flight, not the entry - "
f"{skipped} selected entries skipped")
missing = sum(len(by_name.get(n, [])) != 1 or
by_name[n][0].get("status") in ("notrun", "disabled") for n in selected)
if missing:
raise ValueError(f"{label} control: {missing} selected entries did not run")
not_failed = sum(c.find("failure") is None or c.get("status") != "fail"
for n in selected for c in by_name[n])
if not_failed:
raise ValueError(f"{label} control: {not_failed} selected entries did not fail")
elif mode == "assertion":
cases = ET.parse(sys.argv[4]).getroot().findall(".//testcase")
by_name = {case.get("name"): case for case in cases}
for name in selected:
case = by_name.get(name)
output = "" if case is None else " ".join(" ".join(case.itertext()).split())
if not re.search(sys.argv[5], output):
raise ValueError(f"E3(a) FAILED: {name} red lacks its persistent-map push diagnostic")
elif mode == "evidence":
missing = []
label = sys.argv[5] if len(sys.argv) > 5 else ""
for name, path in selected.items():
if any_role_file(path) and re.search(sys.argv[4], read_role_logs(path)):
print(f"private-log evidence: {name}: {path}")
else:
missing.append(f"{name} ({path})")
if missing:
if label:
raise ValueError(f"{label} FAILED: no selected private log carries /{sys.argv[4]}/. "
"The library's own line is the only channel for this: ctest's "
"transcript is a FALSE ZERO for library output, because the console "
"sink is compiled out of the configurations these lanes run. "
+ ", ".join(missing))
raise ValueError("E1 FAILED: selected private logs lack expected Fatal{BarrierViolation, \"<slot>\"} line: "
+ ", ".join(missing))
else:
raise ValueError(f"unknown mode: {mode}")
raise ValueError(f"unknown mode: {mode}")
if __name__ == "__main__":
+1 -1
View File
@@ -27,4 +27,4 @@ P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn`
21. **配对 / 认证的形式。** **已裁定(ID-P7-3,2026-09-22):令牌足够,不做 TLS。** 威胁模型是「任何人都能连的远程代码路径」而不是窃听:令牌常量时间比较、≥16 字节、无令牌只 loopback、数据面以 `Welcome.dataNonce` 绑定到已认证的控制连接、fork 前认证。TLS 作为 P12 的开放项保留。
22. **client 平台范围。** 默认先 Linux + Android(现有 CI 能验),Windows client 第二批(`ShmSegmentWin32` 在;`pipe:` 具名拒绝作废、TCP 替代),macOS 仍不拆分。这决定 `ARCHITECTURE.md` §15.3 "Windows 机器不是正确性门"要不要改。
23. **半开连接的检测。** Wi-Fi 掉线不产生挂断事件;dl 的闩取自描述符挂断,TCP 上需要 keepalive / 心跳超时并**由传输层报告为挂断**——这与"绝不取自 apply 超时"不矛盾,写清楚免得重吵。P6.5 ct。
24. **E1 对照的重定义(ID-122)。** **已关闭(2026-09-29 债务审查)**:`10bfd29f` 把 E1 的像素竞速换成观测协议等待的负控 `scripts/ci/wait_boundary_negative_control.py`,由 `split_negative_controls.sh:174` 调用。
24. **E1 对照的重定义(ID-122)。** **已关闭(2026-09-29 债务审查)**:`10bfd29f` 把 E1 的像素竞速换成观测协议等待的负控 `scripts/ci/wait_boundary_negative_control.py`。2026-10-18 该负控退役:探针看得见缺失的等待,改由常驻单元用例 `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` 证明(同一个 `ApplierReset`,发布后不等待,必须读到 parked=0)。
+1 -1
View File
@@ -11,7 +11,7 @@
| 17.1 | verb barrier 与诚实的同地址空间传输(P5) | 同地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`;reply 以记录序号为 slot id(R-3);等待看门狗 120 s 只判死锁,不是性能门 | [`docs/archive/notes/p5/README.md`](../../archive/notes/p5/README.md) |
| 17.2 | 后端槽三类、caps、tight readback(P5) | A 本地答 / B 发射 / C 具名拒绝,永不回落 monolith(R-4);能力只读 `CallMask`(R-8);`ReadPixels` 线上恒 tight,client 按自己的 pack state 散射 | 同上 |
| 17.3 | server 角色、shadow 与退出(P5) | apply 线程终身持有 native context,EGL 罕见操作经控制邮箱进 apply 线程;staged shadow 只交 server-owned 拷贝;有界退出顺序 | 同上 |
| 17.4 | lane 隔离(P5) | 每条 split 条目私有日志;E1 / E3 控制把"选中条目被跳过"判为失败 | 同上 |
| 17.4 | lane 隔离(P5) | 每条 split 条目私有日志;E1 与 E3(a) 的对照是常驻用例(`RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`、`PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`),不重跑 lane | 同上 |
| 17.5 | class-C verb 迁移(P5b) | 迁移顺序由动态普查决定;一次迁移 = 一个发射器 + 一个 sink 体;记录逐字带 GL 调用参数,未测量的形式按名拒绝(规则 D) | [`docs/archive/notes/p5b/README.md`](../../archive/notes/p5b/README.md) |
| 17.6 | 共享地址空间访问归零(P5c) | server 端纹理 staged shadow;`SEG_EVENT` 是唯一反向通道;sink / twin 按记录句柄解析;`applier_reset` / `object_death` 控制记录;角色守卫 `Fatal{RoleViolation}` | [`docs/archive/notes/p5c/README.md`](../../archive/notes/p5c/README.md) |
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照,`VERB_BARRIER=0` 会连带关掉 run-ahead | [`docs/archive/notes/p5e/README.md`](../../archive/notes/p5e/README.md) |
+3
View File
@@ -54,6 +54,9 @@ them wrong.
| 2026-10-17 | `Magma split lane (strict, dual-block)` on the disaggregation branches when the dual-block step proved its defaults | Test / `integration-split-strict` | the dual-block census run covers the `integration-magma-split` label in the same environment | the dual-block census run |
| 2026-10-17 | the 75 retrace-split inproc rows other than OpenRA's, when `build-linux-split` reuses `build-linux` | Test / `retrace-split` | the retrace job's inproc-acceptance rows replay the same cases on the same library, strict and with the dual block and run-ahead on; no one-sided pair since 10-05 | the inproc-acceptance rows, which now prove per leg from the client log that they run with strict=1, role-split-state=1 and run-ahead=1 (`MGTRACE_EXPECT_IPC`) and keep a refusal census; every remaining retrace-split leg proves its defaults (strict=0, role-split-state=1, run-ahead=1) and a changed default reds every leg with the rows to restore; `trace_cases.py --self-test-split-matrix` (disaggregation branches) proves every dropped row is an acceptance row; OpenRA's inproc rows stay for the wire evidence |
| 2026-10-17 | `Transport resolution check`'s grep of one entry's log for the inproc resolution sentence | Test / `integration-split` | it checked one entry (`DirectGLES.Split.PersistentMapArm.`) after the fact | in-test: the split fixture (`Harness/ScenarioFixture.h`) now fails every armed Split/Spawn/Tcp entry whose process did not resolve the transport its registration claims, or whose server is not where the claim says (a thread here for inproc, a process of its own for spawn and tcp), from the running process (`SplitRuntimePeek`, `ClientSession::PeerServerPid()`); `SplitLaneHarness.TransportClaimRule` (unit) proves the rule red on canned inputs; the step keeps the IPC default proof as `Default-knob resolution check` |
| 2026-10-18 | `Negative controls (barrier, map push)` (`scripts/ci/split_negative_controls.sh`; skip-census lane `junit-integration-split/control`) | Test / `integration-split` | it reran the split entries with `MOBILEGL_IPC_VERB_BARRIER=0` and `MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0`, two product knobs that exist only for it; what was left without them (the ownership check and the baseline tally) repeats `SplitLogPaths.PrivateAndDistinct` and the lane step's own `junit_tally.py --require-split-ran` | E1: `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` (unit, always on); E3(a): `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer` on the inproc, spawn, tcp and small-ring split entries (always on) |
| 2026-10-18 | `scripts/ci/wait_boundary_negative_control.py` | Test / `integration-split` | E1's body inside the step above: the three boundary cases rerun with the barrier off | `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`: the same ApplierReset published with no wait must read parked=0, the observation the owed waits read as parked=1 |
| 2026-10-18 | `scripts/ci/control_smoke_test.sh`, `scripts/ci/testdata/stub_ctest.sh`, `scripts/ci/testdata/split_private_log_smoke.sh`, and the `reset` / `results` / `assertion` / `evidence` modes of `split_log_paths.py` | Test / pipe-gates (R-16) | smoke cases and helpers of the step above only | none needed |
## Kept after investigation
-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",