diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f1a78ca..2a31cf62 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3287,12 +3287,9 @@ jobs: echo "::error::MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters left the OpenRA retrace GREEN, so the comparator is not comparing and the whole verify retrace lane proves nothing." exit 1 fi - # Its own red: ctest -V prefixes every line with the test number, and CMake re-wraps text. - if ! sed -E 's/^[0-9]+: //' "${RUNNER_TEMP}/verify-control.txt" | tr -s '[:space:]' ' ' \ - | grep -qF 'Fatal{PipeVerifyDiffer, "GetRenderStateParameters@'; then - echo "::error::the corrupted-snapshot retrace went red without Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@...\"}: something other than the comparator failed it" - exit 1 - fi + # Its red must carry the comparator's divergence on the corrupted field + # (scripts/ci/retrace_verify_reason.py, which test_run_trace_case.py runs on canned output). + python3 "${GITHUB_WORKSPACE}/scripts/ci/retrace_verify_reason.py" "${RUNNER_TEMP}/verify-control.txt" echo "the corrupted field turned the retrace red, as it must (ctest exit ${control_rc})" - name: Upload core dumps diff --git a/scripts/ci/retrace_verify_reason.py b/scripts/ci/retrace_verify_reason.py new file mode 100644 index 00000000..290bde29 --- /dev/null +++ b/scripts/ci/retrace_verify_reason.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# MobileGL - scripts/ci/retrace_verify_reason.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 + +"""Whether retrace-verify's corrupted-snapshot control went red for its own reason. + +The control replays OpenRA with MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters and must go +red with the comparator's divergence on that field: a red without it (another Fatal, a divergence +on another field) proves nothing about the comparator. The check requires that divergence in the +transcript; it does not rule out other failures beside it. run_trace_case.cmake prints the library +log through message(STATUS), which never re-wraps, so each log line stays one transcript line. + +Usage: retrace_verify_reason.py TRANSCRIPT (the control run's ctest -V output) +Exit 0 when the divergence is there, 1 when it is not, 2 when the transcript cannot be read. +""" +import sys + +REASON = 'Fatal{PipeVerifyDiffer, "GetRenderStateParameters@' + + +def carries_reason(text): + return REASON in text + + +def main(argv=None): + argv = sys.argv[1:] if argv is None else argv + if len(argv) != 1: + print(__doc__) + return 2 + try: + text = open(argv[0], encoding='utf-8', errors='replace').read() + except OSError as error: + print(f'::error::retrace_verify_reason: cannot read {argv[0]}: {error}') + return 2 + if carries_reason(text): + print(f'retrace_verify_reason: the red carries {REASON}...') + return 0 + print(f'::error::the corrupted-snapshot retrace went red without {REASON}...: ' + 'something other than the comparator failed it') + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/trace_replay/test_run_trace_case.py b/tools/trace_replay/test_run_trace_case.py index 3400fdc7..c94bd72f 100644 --- a/tools/trace_replay/test_run_trace_case.py +++ b/tools/trace_replay/test_run_trace_case.py @@ -29,6 +29,7 @@ import unittest HERE = Path(__file__).resolve().parent SCRIPT = HERE / 'run_trace_case.cmake' CHECKER = HERE.parents[1] / 'scripts' / 'ci' / 'retrace_split_evidence.py' +VERIFY_REASON = HERE.parents[1] / 'scripts' / 'ci' / 'retrace_verify_reason.py' sys.path.insert(0, str(CHECKER.parent)) import retrace_split_evidence as checker # noqa: E402 @@ -60,10 +61,6 @@ with open(os.path.join(out, 'result.json'), 'w') as result: json.dump({'passed': True, 'ssim': 1.0}, result) ''' -# The retrace-verify control's reason check, as the workflow runs it on the tee'd ctest -V output. -VERIFY_CONTROL_REASON = ("sed -E 's/^[0-9]+: //' \"$1\" | tr -s '[:space:]' ' ' " - "| grep -qF 'Fatal{PipeVerifyDiffer, \"GetRenderStateParameters@'") - PREFIX = '[12:00:00] [Linux mobilegl_trace_/INFO]: ' MARKER = { 'inproc': 'Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real ring to an apply thread', @@ -307,17 +304,24 @@ class RunTraceCaseTests(unittest.TestCase): def test_v4_an_aborted_verify_run_names_its_divergence(self): # The comparator aborts the replay before result.json, so the script dumps the library log; - # the retrace-verify control requires that dump to carry the divergence it caused. + # the retrace-verify control requires that dump to carry the divergence it caused, through + # the same scripts/ci/retrace_verify_reason.py the workflow runs. Another Fatal is not it, + # and neither is the comparator's divergence on a field the control did not corrupt. fatal = 'MGPipe: Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear"} field=3 where=entry' + other = 'MGPipe: Fatal{UnmigratedPipeInput, "GetPixelStoreParameters@ReadPixels"}' + other_field = 'MGPipe: Fatal{PipeVerifyDiffer, "GetPixelStoreParameters@ReadPixels"} field=1 where=entry' env = {'MOBILEGL_PIPE_VERIFY': '1', 'STUB_ABORT': '1'} rc, output = self.run_case(env, {'mobilegl.log': lines('MGPipe: verify armed', fatal)}) self.assertNotEqual(rc, 0, output) transcript = self.root / 'control.txt' - for log, expected in ((lines('MGPipe: verify armed', fatal), 0), (lines('MGPipe: verify armed'), 1)): + for log, expected in ((lines('MGPipe: verify armed', fatal), 0), (lines('MGPipe: verify armed'), 1), + (lines('MGPipe: verify armed', other), 1), + (lines('MGPipe: verify armed', other_field), 1)): _, raw = self.run_case(env, {'mobilegl.log': log}, squashed=False) transcript.write_text(''.join('1: ' + row + '\n' for row in raw.splitlines())) - check = subprocess.run(['bash', '-c', VERIFY_CONTROL_REASON, 'reason', str(transcript)]) - self.assertEqual(check.returncode, expected, raw) + check = subprocess.run([sys.executable, str(VERIFY_REASON), str(transcript)], + capture_output=True, text=True) + self.assertEqual(check.returncode, expected, raw + check.stdout) def test_v3_verify_green(self): rc, output = self.run_case({'MOBILEGL_PIPE_VERIFY': '1'}, {'mobilegl.log': lines('MGPipe: verify armed')})