diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 4ebf41d2..5c8e6384 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -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() diff --git a/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in new file mode 100644 index 00000000..d8cdd949 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/SplitLogPaths.cmake.in @@ -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. diff --git a/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py new file mode 100644 index 00000000..41a82ecc --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Harness/split_log_paths.py @@ -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, \"\"} 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}") diff --git a/scripts/ci/split_negative_controls.sh b/scripts/ci/split_negative_controls.sh index d0a66fb2..6baf5d5e 100755 --- a/scripts/ci/split_negative_controls.sh +++ b/scripts/ci/split_negative_controls.sh @@ -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, ""}. +# 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 diff --git a/scripts/ci/testdata/split_private_log_smoke.sh b/scripts/ci/testdata/split_private_log_smoke.sh new file mode 100644 index 00000000..cf226eea --- /dev/null +++ b/scripts/ci/testdata/split_private_log_smoke.sh @@ -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" diff --git a/scripts/ci/testdata/stub_ctest.sh b/scripts/ci/testdata/stub_ctest.sh index ba6ff54a..de9dba3e 100755 --- a/scripts/ci/testdata/stub_ctest.sh +++ b/scripts/ci/testdata/stub_ctest.sh @@ -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' '' "" " ${body}" '' > "$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 + 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) + 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"