[Fix] (IntegrationTest): give Split entries private diagnostic logs

This commit is contained in:
2026-09-16 08:48:30 -04:00
parent c3e736a343
commit b2dcae713f
6 changed files with 183 additions and 34 deletions
@@ -1906,6 +1906,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# target B and PersistentCoherentMap is target C, both new in P5.
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST MGL_SPLIT_CLEAR_TESTS
TEST_FILTER "ClearThenReadPixelsScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
@@ -1915,6 +1916,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST MGL_SPLIT_TRIANGLE_TESTS
TEST_FILTER "TriangleScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
@@ -1924,6 +1926,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split."
TEST_LIST MGL_SPLIT_PMAP_TESTS
TEST_FILTER "PersistentCoherentMapScenario.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
@@ -1947,6 +1950,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split.PersistentMapArm."
TEST_LIST MGL_SPLIT_ARM_TESTS
TEST_FILTER "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares"
DISCOVERY_TIMEOUT 30
PROPERTIES
@@ -1984,6 +1988,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
PersistentCoherentMapScenario)
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.Split.SmallRing."
TEST_LIST "MGL_SPLIT_SMALL_${mglItestSmallRingScenario}_TESTS"
TEST_FILTER "${mglItestSmallRingScenario}.*"
DISCOVERY_TIMEOUT 30
PROPERTIES
@@ -1992,4 +1997,13 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
ENVIRONMENT "${MGL_ITEST_GLES_SPLIT_SMALL_RING_ENVIRONMENT}"
)
endforeach()
# Apply per-entry paths after GoogleTest discovery has populated the TEST_LISTs.
configure_file(Harness/SplitLogPaths.cmake.in SplitLogPaths.cmake @ONLY)
set_property(DIRECTORY APPEND PROPERTY TEST_INCLUDE_FILES
"${CMAKE_CURRENT_BINARY_DIR}/SplitLogPaths.cmake")
find_package(Python3 REQUIRED COMPONENTS Interpreter)
add_test(NAME SplitLogPaths.PrivateAndDistinct
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/Harness/split_log_paths.py"
check "${CMAKE_CTEST_COMMAND}" "${CMAKE_BINARY_DIR}")
endif()
@@ -0,0 +1,14 @@
# Included by CTest after all GoogleTest discovery files (ID-53).
# CTest appends ENVIRONMENT here; keep all previously discovered lane settings.
file(MAKE_DIRECTORY "@CMAKE_CURRENT_BINARY_DIR@/split-logs")
foreach(entry IN LISTS MGL_SPLIT_CLEAR_TESTS MGL_SPLIT_TRIANGLE_TESTS MGL_SPLIT_PMAP_TESTS)
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
endforeach()
foreach(scenario ClearThenReadPixelsScenario TriangleScenario PersistentCoherentMapScenario)
foreach(entry IN LISTS MGL_SPLIT_SMALL_${scenario}_TESTS)
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MOBILEGL_LOG_FILE_PATH=@CMAKE_CURRENT_BINARY_DIR@/split-logs/${entry}.log")
endforeach()
endforeach()
# PersistentMapArm retains its existing private path and RESOURCE_LOCK: b1 reads it.
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Validate discovered Split log ownership; read only freshly reset control logs."""
import json
from pathlib import Path
import re
import subprocess
import sys
def paths(document):
owners = {}
split = {}
for test in document["tests"]:
props = {p["name"]: p["value"] for p in test.get("properties", [])}
values = [v.split("=", 1)[1] for v in props.get("ENVIRONMENT", [])
if v.startswith("MOBILEGL_LOG_FILE_PATH=")]
is_split = "integration-split" in props.get("LABELS", [])
if is_split and (len(values) != 1 or not values[0]):
raise ValueError(f"{test['name']}: requires exactly one nonempty MOBILEGL_LOG_FILE_PATH")
for value in values:
path = str(Path(value).resolve())
owners.setdefault(path, []).append(test["name"])
if is_split:
if not Path(value).is_absolute():
raise ValueError(f"{test['name']}: MOBILEGL_LOG_FILE_PATH must be absolute: {value}")
split[test["name"]] = path
if not split:
raise ValueError("integration-split: no entries discovered")
for name, path in split.items():
if len(owners[path]) != 1:
raise ValueError(f"{name}: duplicate MOBILEGL_LOG_FILE_PATH {path}: {owners[path]}")
return split
def main():
mode = sys.argv[1]
if mode == "check":
data = subprocess.check_output([sys.argv[2], "--test-dir", sys.argv[3],
"--show-only=json-v1"], text=True)
selected = paths(json.loads(data))
print(f"SplitLogPaths: {len(selected)} entries, {len(set(selected.values()))} distinct private paths")
return
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)}
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 == "evidence":
for name, path in selected.items():
if Path(path).is_file() and re.search(sys.argv[4], Path(path).read_text(errors="replace")):
print(f"private-log evidence: {name}: {path}")
return
raise ValueError("E1 FAILED: selected private logs lack expected Fatal{BarrierViolation, \"<slot>\"} line: "
+ ", ".join(f"{n} ({p})" for n, p in selected.items()))
else:
raise ValueError(f"unknown mode: {mode}")
if __name__ == "__main__":
try:
main()
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit(f"SplitLogPaths FAILED: {error}")
+31 -30
View File
@@ -33,21 +33,29 @@
# 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. (It is also unreachable from here: the three
# DirectGLES.Split. lanes set no MOBILEGL_LOG_FILE_PATH, and the library's console sink is compiled
# out of this configuration, so no MGLOG_ output of any level reaches ctest's transcript. Measured:
# ~/w7/p5-v1-joint-isplit-barrier0.log, 18 aborted entries, zero occurrences of the string "Fatal".)
# Only the failing case's own diagnostic does that. ID-53 gives each Split entry a private
# library log: E1 reads its Fatal there; E3(a) reads the scenario assertion in ctest output.
#
# Usage: split_negative_controls.sh
# Usage: split_negative_controls.sh [--self-test]
# CTEST ctest binary (default: ctest)
# CONTROL_TMPDIR scratch dir for the junit + output (default: ${RUNNER_TEMP:-/tmp})
set -u
if [ "${1:-}" = "--self-test" ]; then
bash "$(dirname "$0")/control_smoke_test.sh" &&
bash "$(dirname "$0")/testdata/split_private_log_smoke.sh"
exit $?
fi
CTEST="${CTEST:-ctest}"
CONTROL_TMPDIR="${CONTROL_TMPDIR:-${RUNNER_TEMP:-/tmp}}"
mkdir -p "${CONTROL_TMPDIR}"
junit="${CONTROL_TMPDIR}/isplit.xml"
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 ---------------------------------------------------------------------------
#
@@ -101,6 +109,11 @@ run_control() {
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"
env "$@" "${CTEST}" --output-on-failure -L integration-split -R "${filter}" --no-tests=error > "${out}" 2>&1
control_rc=$?
@@ -111,41 +124,29 @@ run_control() {
exit 1
fi
# THE HALF THAT WAS MISSING. A non-zero exit is necessary and nowhere near sufficient.
# Whitespace is normalised across the whole file first, for the reason given in
# retrace_pull_library_control.sh: a diagnostic that arrives wrapped is still the diagnostic.
if ! tr -s '[:space:]' ' ' < "${out}" | grep -qE "${evidence}"; then
echo "::error::${name} turned ${matched} selected entries red, but the red carries NONE of the diagnostics those scenarios emit when this knob is off, so it is not this control's red. Required one of: ${evidence}. A timeout, a setup abort, a harness that died before it read the knob, or any unrelated assertion lands here - and every one of them used to print the success message below and leave this step green (ID-46 finding 8). If the entries aborted with no output at all, that is the barrier path having no named diagnostic of its own: see t1-v2.md, it is a debt on the server package, not a reason to accept the red."
# E1's MGLOG_F sink is the private file, never ctest's status or transcript.
if [ "${evidence}" = "private-barrier-fatal" ]; then
python3 "${log_helper}" evidence "${manifest}" "${filter}" \
'Fatal\{BarrierViolation, "[A-Za-z_][A-Za-z_0-9]*"\}' || exit 1
elif ! tr -s '[:space:]' ' ' < "${out}" | grep -qE "${evidence}"; then
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
exit 1
fi
echo "${name} turned ${matched} selected entries red, and the red carries the scenario's own diagnostic, as it must"
}
# E1: R-1's lockstep verb barrier. Without it the client keeps pulling fields from a live GLContext
# while the server runs ahead, so the server reads future values.
#
# THE SELECTION INCLUDES THE SmallRing LANE, and that is not cosmetic. Measured on v1's joint tree
# (~/w7/p5-v1-joint-isplit-barrier0.log): with the barrier off, every entry the OLD filter selected
# aborted with no output whatsoever, and the one entry in the whole run that failed with a readable
# assertion - ClearThenReadPixelsScenario.cpp:290/:295, reading back 0 where >200 was cleared - was
# a DirectGLES.Split.SmallRing. entry, which the old filter excluded. A control whose selection
# contains no case able to say why it failed cannot assert its own failure reason. The SmallRing
# entries are the same two scenarios under the same transport with SEG_CMD/SEG_STAGE at their floor,
# so including them widens E1's selection strictly within E1's charter.
# E1: c1 ClientSession::Post emits MGLOG_F Fatal{BarrierViolation, "<slot>"}.
# Keep the ID-53-approved SmallRing selection as well as the default lane.
run_control "negative control E1 (MOBILEGL_IPC_VERB_BARRIER=0)" \
'DirectGLES\.Split\.(SmallRing\.)?(Triangle|ClearThenReadPixels)' \
'ClearThenReadPixelsScenario\.cpp:(290|295)|the bottom band should be red after the resolve|the top band should be blue after the resolve|TriangleScenario\.cpp:[0-9]+: Failure' \
'private-barrier-fatal' \
MOBILEGL_IPC_VERB_BARRIER=0
# E3(a): the persistent-map push. 0 is admitted by ConfigLoader on purpose and is documented there
# as this control. The evidence is the scenario's own wording for "the second write never arrived":
# with the push off, the write that no GL call announces cannot reach its draw, which is exactly
# what TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw and
# AWriteAfterAFrameBoundaryReachesTheNextFramesDraw read back
# (PersistentCoherentMapScenario.cpp:414-417, :442-443). The counting case's pmap= assertion
# (:531-541) is listed too, for the lane where it is the one that runs.
# E3(a): PersistentMapTracker.cpp returns at blockBytes == 0 (no Fatal).
# PersistentCoherentMapScenario.cpp:414-417 / 442-443 name the missing second write.
# Do not accept a generic source-line Failure: an unrelated assertion is not this red.
run_control "negative control E3(a) (MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0)" \
'DirectGLES\.Split\.(SmallRing\.)?PersistentCoherentMapScenario' \
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping|cannot have pushed|PersistentCoherentMapScenario\.cpp:[0-9]+: Failure" \
"the SECOND write through the same mapping, announced by nothing|frame 1's write through the SAME mapping, after a Present" \
MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# Exercise the production control, including the private-file read (ID-53 / 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; 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 private-file Fatal must PASS"; exit 1;
}
else
message='E1 FAILED: selected private logs lack expected Fatal'
[ "${mode}" != e3-unrelated ] || message='FAILED: red lacks its persistent-map push diagnostic'
[ "${rc}" != 0 ] && grep -q "${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"
+30 -4
View File
@@ -11,6 +11,10 @@
# 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
# 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 (the disarmed lane, a legitimate exit 0)
@@ -26,6 +30,7 @@ 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
prev="$a"
@@ -57,6 +62,16 @@ write_junit() {
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).
log="${CONTROL_TMPDIR}/entry.log"
if [ "${json_requested:-0}" = 1 ]; then
python3 -c 'import json, os; p=os.environ["CONTROL_TMPDIR"]; 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 [("ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels", "entry.log"), ("PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw", "pmap.log")]]}))'
if [ "${mode}" = stale-fatal ]; then
echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}"
fi
exit 0
fi
if [ "${listing_requested}" = "1" ]; then
emit_listing
exit 0
@@ -71,16 +86,27 @@ if [ -n "${junit}" ]; then
fi
# The control's own run.
if [ "${MOBILEGL_IPC_VERB_BARRIER:-1}" = 0 ]; then
case "${mode}" in
unrelated)
evidence|e3-unrelated) echo 'Fatal{BarrierViolation, "DrawVbo"}' > "${log}" ;;
missing-fatal) echo "library setup only; no fatal" > "${log}" ;;
stdout-fatal) echo 'Fatal{BarrierViolation, "DrawVbo"}' ;;
esac
fi
case "${mode}" in
unrelated|missing-fatal|stdout-fatal|stale-fatal|e3-unrelated)
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)
# Both controls' required wording, so one stub serves E1 and E3(a). Copied from the real
# diagnostics: ~/w7/p5-v1-joint-isplit-barrier0.log for the first, and
# PersistentCoherentMapScenario.cpp:414-417 for the second.
# 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"