[M0-TEST-01] Test infrastructure conventions: docs/testing.md + seed handling + SeededRandom KAT suite + fuzz lane semantics - #13
Merged
Conversation
…andom KAT suite + fuzz lane docs - docs/testing.md: normative test conventions (layout mirroring src/, <module>_tests executables, regress_<short-id> regression tests, laige-fuzz target registration + CI lane semantics, seed handling) - tests/support/laige_test_seed.h: TestSeed()/TestPrng() — fixed default seed 0x1F055EED (same as laige-fuzz), LAIGE_TEST_SEED env override, loud failure on invalid value (CORE-008), per-test substream ids - tests/testing: test_infra_tests executable + test_infra CTest entry, SeededRandom suite (6 cases): 65536-draw FNV-1a KATs under default and override seeds, first-draw KAT, seed identity, invalid-env loud failure (EXPECT_NONFATAL_FAILURE), seed parser, substream isolation; machine-greppable test-seed-check line per KAT for the two-CI-runs identity check (M0-TEST-01 Verify) - first regress_ test: M0-CORE-08's ConfigJsonValid.ObjectMemberWhitespace renamed regress_json_object_member_ws (TEST-003 convention) - fuzz lane: bounded --runs=1000 = existing fuzz_json_parse ctest entry in every P0 job; nightly long form --runs=1000000 documented in docs/testing.md and the canonical command table - docs wired: docs/README.md, tests/README.md, building.md, README.md, CI workflow header notes
…angelog - roadmap/M0-foundations.md: M0-TEST-01 checked with Decision/Verify/Size (docs/testing.md conventions; seed helper + SeededRandom KAT suite; first regress_ test; fuzz lane semantics; local 32/32 on five trees; the two-CI-runs identity check recorded in the follow-up CI-observation commit) - roadmap/README.md: progress board M0 19 -> 20 done (total row 17 -> 20, correcting the stale total), changelog row for M0-TEST-01 (a292aef)
…portability Discovered by this step's first full-matrix CI run (ci.yml run 34713475074 on the branch): the windows-msvc job builds the whole tree, and this was the first run in which it did — the M0-TOOL-02 PR run had windows-msvc label-skipped, so the detcheck bug below was never exercised before. - tools/detcheck/laige-detcheck.cpp: windows.h's WinDef.h defines the min/max macros, which broke std::min in compareStreams (MSVC C2589 / C2059 / C2737). Define NOMINMAX before including windows.h (CPP-009: compile-time platform boundary). - tests/support/laige_test_seed.h: ReadEnvVar() platform boundary — MSVC deprecates plain getenv (C4996, fatal under /WX); use getenv_s with the same lookup semantics (the established pattern of tests/laige-core/budget_harness_tests.cpp and tools/bench/laige-bench.cpp). - tests/testing/seeded_random_tests.cpp: ScopedTestSeedEnv — MSVC's CRT has no setenv/unsetenv (C2039/C3861); use _putenv_s (the secure variant, no C4996) with _putenv_s(name, "") as the unset equivalent (TestSeed() treats an empty value exactly as unset). - roadmap/README.md: repair the M0-TOOL-02/M0-TEST-01 changelog rows that a prior edit merged into one line. Local re-verification: 32/32 ctest, zero warnings, on all five local trees (g++ static/shared, ASan+UBSan, TSan, Clang).
The first full-matrix CI run (ci.yml run 34714125493 on the branch) got the windows-msvc build through the NOMINMAX fix, but four detcheck cross-binary tests failed with 'scenario process exited with code 259' while the same fixture binary succeeded in other tests of the same run. 259 is ERROR_FILE_NOT_FOUND as a child exit code, which should not be reachable from the fixture main() — the message needs the attempted command and a file-existence check to diagnose it: - GetFileAttributesW before CreateProcessW: a missing executable is now reported as such (lastError included), instead of surfacing as a numeric child exit code. - The non-zero child exit message now includes the attempted command line (LOG-002: errors state what failed and with which input). Local: 32/32 ctest on g++/ASan/Clang trees; cross-binary smoke test OK. The diagnostic run will run on Windows in CI next.
…failures Run 34715705611 (full matrix on the branch) confirmed the spawn diagnostics: the attempted command exists (GetFileAttributesW found it) yet the child exited with 259 (ERROR_FILE_NOT_FOUND) while the same fixture binary succeeded in other tests of the same run — the signature of the Windows first-launch image-load race right after a fresh build (a file filter, e.g. Windows Defender, still scanning the new .exe). - runScenario (Windows) is now runScenarioOnce plus a bounded retry: one run that produced no output and exited with an OS image-load failure code (259/32/126/142/193/1422) is retried exactly once after a 250 ms delay. - The retry cannot mask scenario behavior: a deterministic scenario fails identically on the retry and is still reported as exit 2; a run with captured output is never retried. - docs/api/detcheck.md documents the Windows retry (DOC-003). Local: 32/32 ctest on g++/ASan/Clang trees (POSIX path unchanged).
…dows retry The one-retry fix (aaa2725) was not sufficient: on run 34716094107 the retry fired (test durations jumped 0.03s -> 0.3s) and recovered some launches (detcheck-bin-malformed / -scenario-failure passed via the retry) but detcheck-bin-identical / -args / -diverged / -stream-mismatch still failed with 259 on BOTH attempts, while the same fixture binary succeeded in other tests of the same run. The failure rate is high and shifts between runs, so the retry needs data to be tuned: - stderr line when the retry fires (attempt 1 exit code). - stderr line on final failure: attempt count + the executable's last-write time (unix seconds), distinguishing a still-being-written file from a scan/contention window on a finished file. Local: ctest 32/32 (POSIX path unchanged).
…xit code Root cause of the 259: GetExitCodeProcess was called without first waiting for the child. A process whose image load failed (freshly written .exe still held by a file filter, e.g. Windows Defender) never reaches a terminated state at the moment the pipe closes, so GetExitCodeProcess returned STILL_ACTIVE — 259 — which we misread as the scenario's exit code. The POSIX path already had the equivalent waitpid; the Windows path did not. - WaitForSingleObject(INFINITE) before GetExitCodeProcess (mirrors the POSIX waitpid; ctest's 120s per-test timeout remains the backstop). - The transient retry now matches the codes the child actually reports on image-load failure (0xC0000142, 0xC0000366, plus the Win32 set), since 259/STILL_ACTIVE is no longer reachable. - Header + docs/api/detcheck.md updated to match (DOC-003). Local: ctest 32/32 (POSIX path unchanged).
…cause) Root cause of all the Windows detcheck failures (found by the diagnostics in ff309b8): the capture loop treated a bare PeekNamedPipe n==0 as 'pipe closed', but n==0 also occurs while the child is still starting up and has not written yet. Breaking there closed the pipe under a live child; the child's stdout writes then failed (broken pipe) and it exited 0 with all output lost. Without the child-termination wait added earlier, GetExitCodeProcess on that still-running child returned STILL_ACTIVE — the 259 we chased. - The loop now WaitForSingleObject(readH) before each PeekNamedPipe; a pipe signals when data is available OR the write end closes, so n==0 after the wait genuinely means the scenario finished. - The WaitForSingleObject(process) before GetExitCodeProcess is kept (still required for a definite exit code; mirrors POSIX waitpid). - The speculative first-launch retry is removed: it was treating the symptom of this bug, and no retry can be justified once the capture is correct (CORE-004). The file-existence diagnostic and the attempted command in the failure message stay (LOG-002). - Header + docs/api/detcheck.md updated to match (DOC-003). Local: ctest 32/32 on g++/ASan/Clang trees; cross-binary smoke test OK (divergence at tick 7 as expected).
The pipe-wait fix (6874978) did not resolve the Windows failures: the child now terminates (exit 0) with no output captured, so the spawn is succeeding but the child's stdout is not our pipe in the failing cases. Record the child's pid and the image path the kernel actually started (QueryFullProcessImageNameW) on every launch — the CI log will show which process the failing launches really are.
The run-11 diagnostics (child pid + image path) showed the failing launches create a child process that exits 0 with no stdout captured, while later launches of the same binary succeed — the signature of the runner's file filter (AV) first-open scan on freshly built .exe files: the first process start of a new file is disrupted, later starts are cached and fine. Add a 'Warm up scenario binaries' step to the windows-msvc job (ci.yml and the label-gated ci-pull.yml job): run laige-detcheck (synthetic), laige-fuzz (one run), and each detcheck fixture variant (--ticks=1) once, right after the build, so the first-open scans are absorbed outside the ctest assertions. The per-launch child pid/image diagnostic stays (LOG-002).
The warm-up step did not change the failure pattern: the first ctest launches of the fresh fixture binaries still exit 0 with no stdout captured while later launches of the same binaries succeed. Probe the capture machinery itself before every scenario spawn: cmd.exe /c echo through the identical pipe/STARTUPINFO sequence. If the probe captures its marker while a scenario run does not (or vice versa), the CI log localizes the broken side of the launch.
The control probe (bfc8b88) failed in exactly the same process instances as the scenario runs (cmd.exe echo captured empty), so the capture machinery is broken per-process on the Windows runner — not the fixture binaries. To compare failing vs passing process instances, dump the CWD and environment (names always; values for LAIGE_/CTEST_ variables and PATH) before every scenario spawn.
The env dump showed the LAIGE_* variables ARE in the child's environment (the 'missing' ones were a display artifact of my own dump format), so env inheritance works and the broken side is the pipe handle inheritance / STARTUPINFO delivery of spawned children in some process instances. - probeAttempt(): shared spawn+capture with logged pipe handle values, GetFileType results, and the write handle's flag info. - probeControlSpawn(): attempt 1 as before (command-line first token); attempt 2, only when attempt 1 captures nothing, passes the explicit System32 cmd.exe path via lpApplicationName. If attempt 2 succeeds in the instances where attempt 1 fails, first-token resolution is the broken side. - runScenario(): scenario launch now uses the explicit exe path as lpApplicationName (strictly safer), plus the same handle logging. - Check script prints the tool output on success too (temporary) so passing instances can be compared with failing ones.
Root cause found: the run-18 handle logging showed every failing
instance with writeFlags=0x1 on the pipe's write end - the
HANDLE_FLAG_INHERIT bit (0x80) is NOT set, even though
SetHandleInformation(writeH, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)
returned success (no error in runScenario). A child created with
bInheritHandles=TRUE cannot inherit a non-inheritable handle, so the
child never received our pipe's write end as its stdout: its writes
went to a broken pipe ('The process tried to write to a nonexistent
pipe'), it exited 0, and the capture saw n==0 - the 'no ticks
emitted' failures. Passing instances are the ones where the flag
happened to be set.
Fix: set sa.bInheritHandle=TRUE when creating the pipe, so the
handles are inheritable from creation; keep the SetHandleInformation
call (harmless, and its failure is still checked) and log its result
and the resulting handle flags on every launch.
Run 19 showed the bInheritHandle=TRUE creation did not change the failure pattern, and writeFlags=0x1 (no visible INHERIT bit) persists in every failing instance even with inheritable creation — so the handle-flag reading alone cannot distinguish working from broken instances. Get the ground truth from the child side instead. - laige-detcheck-probe (new, Windows-only): a diagnostic child that reports the std handles the kernel actually assigned to it (value/type/flags) on stderr and writes a marker to stdout. - probeControlSpawn: four attempts per process instance — helper via command-line path, helper via explicit lpApplicationName, the classic cmd.exe marker, and helper with NO STARTUPINFO redirection (plain handle inheritance control). Both stdout and stderr are captured on separate pipes and logged per attempt. - Diagnostic sink: every diagnostic line goes to stderr AND a buffer flushed to $LAIGE_DETCHECK_DIAG_FILE (per-test, set by CTest's ENVIRONMENT) on every exit path — ctest hides passing-test output, so this is how a passing instance's diagnostics reach the log. - Windows CI test step cats the per-test diag files after ctest (exit code preserved); the probe helper is added to the warm-up step. - Env dump trimmed to the test-wiring variables only (the full name dump was log noise; the wiring is confirmed intact). Probes run once per process instance, not per scenario run.
…, capture mirror Run 21's per-test diag files exposed a contradiction: test 27 (detcheck-bin-malformed) PASSED - which requires its check-script detcheck invocation to have captured both scenario runs, including run-b's malformed line - yet that test's diag file ends with 'scenario run-a: no ticks emitted', which only a capture-broken instance can produce. A diag file can be truncated and rewritten by a LATER detcheck invocation that shares the same LAIGE_DETCHECK_DIAG_FILE, so the writer must be identified. - diag begin now logs this process's pid, its parent's pid, and the parent's command line (Windows: NtQueryInformationProcess via GetProcAddress, best-effort; POSIX: getppid + /proc cmdline). - The check script tags its detcheck invocation with LAIGE_DETCHECK_CALLER=check-script (logged by the env dump) and mirrors its own capture to <name>.txt.check, written by the cmake process, not detcheck - so both writers' output reach the job log even though ctest hides passing-test output. - The Windows CI test step now cats all per-test diag files (*.txt and *.check), not just *.txt.
- The check script's detcheck caller tag now rides in the test's ENVIRONMENT property (inherited by execute_process) instead of the execute_process ENVIRONMENT option: that option does not exist on the cmake versions some CI runners provide (the ubuntu-24.04 lanes reject it as an unknown argument; CTest's ENVIRONMENT_MODIFICATION test property is a different, newer API). - diag begin uses GetCurrentProcessId() directly: the GetProcessId macro form fails on the windows-2022 SDK (C2660, the macro does not expand there as a zero-argument function).
…ouble-escape The generated check scripts (tests/api, tests/detcheck) asserted ONLY the exit code: their output-fragment checks were silently absent in every generated script, on every platform, since M0-TOOL-02/M0-TOOL-01. Two independent defects in the generators: 1. Variable name mismatch: the functions built the checks into a local called _checks, but the templates substitute @Checks@. configure_file therefore expanded the placeholder from an unset variable and emitted nothing. Rename the local to CHECKS (the exact placeholder name). 2. Backslash under-escaping: CMake string literals strip one level of backslashes on parse (\\ -> \, \X -> X, verified on CMake 4.4.3). The generated script is itself a CMake file, so each regex escape must reach it DOUBLED: \\X in the file becomes \X for the regex engine. A single \X was stripped and the bare metacharacter reached the regex (a group where a literal was meant, an unclosed class, or a dropped character for [). The [ replacement was additionally losing the bracket itself; it is now built from a _bs2 variable so the literal survives intact. Effect: the suite now actually asserts the fragments it claims to. Verified locally: all generated scripts contain real MATCHES checks; all five P0 trees (build, build-shared, build-asan, build-tsan, build-clang) pass 32/32. Note this also means test 27 (detcheck-bin-malformed) will now fail on the broken-capture Windows instances, where exit code 2 coincidentally matched the expectation: the fragment checks will expose the capture failure that previously went unverified.
…agnosis New evidence from run 23 changed the diagnostic picture: the handle flags logged as 0x1 were all along HANDLE_FLAG_INHERIT (the GetHandleInformation/HANDLE_FLAG_* constants are 0x1 and 0x2, NOT 0x80/0x100), so the pipe write ends WERE inheritable in the failing instances. The 'missing INHERIT bit' theory is dead; the open question is why CreateProcessW still does not deliver the STARTUPINFO pipes while plain handle inheritance (the child inheriting detcheck's own fd0/fd1/fd2) works in the same process instances. To discriminate, probeControlSpawn gains two attempts: - attempt 5: STARTUPINFO with detcheck's OWN kernel-assigned fd1/fd2. If this delivers while the fresh-pipe attempts do not, the failure is specific to handles this process created itself. Success shows up as an extra PROBE_HELPER_OK line in detcheck's own stdout and an extra self-report line in its stderr (capturedOut/capturedErr stay empty by design for this attempt). - attempt 6: STARTUPINFO with a fresh pipe whose write end is first re-created via DuplicateHandle(dwInheritable=TRUE). If this delivers where the original pipe handle does not, the DuplicateHandle path is the fix for scenario capture. Also: the diagnostics now log the handle bits as inherit=.. protect=.. (handleBits helper) instead of raw flag values, and the scenario comment no longer asserts the disproven INHERIT-bit theory. The probe helper is unchanged (it reports raw values; the parent interprets them). Windows-only branch; verified in CI (no local MSVC/mingw available).
Run 24 (34728950395, e2abce5) on the Windows CI runner produced the discriminating data for the STARTUPINFO capture failure: - attempts 5 (detcheck's OWN kernel-assigned fd1/fd2 through STARTUPINFO): DELIVERED - the helper child self-reported fd0=0x234 fd1=0x238 fd2=0x23c (exactly detcheck's handles) and its marker appeared in detcheck's own stdout. - attempts 1-3 (fresh CreatePipe, INHERIT bit confirmed set), 6 (fresh pipe re-created via DuplicateHandle(dwInheritable=TRUE)): NOT delivered - child exited 0 with no output captured. - attempt 4 (no STARTUPINFO, plain inheritance): works (control). So the runner's kernel delivers STARTUPINFO handles only when they are handles detcheck itself inherited from its parent (kernel-assigned), not handles detcheck created - regardless of the INHERIT bit (0x1 = HANDLE_FLAG_INHERIT, confirmed present in every failing instance). The root-cause mechanism of that restriction is outside the process (no local repro); the fix is to stop depending on it. New attempt 7 probes the remaining deliverable candidate on the tool's side: a self-created FILE handle (temp file) as the child's stdout through STARTUPINFO, read back after the child exits. If files deliver where pipes do not, file capture is the scenario path; if not, the capture moves to the check script (child inherits detcheck's fd1, stream delimited by tool-emitted markers). Also this run confirms the dead-fragment-check fix: with the fragment checks live, the Windows failure set is now 24/25/26/27/29 (test 27's coincidental exit-code match no longer masks the broken capture) while 28 still passes (its assertion needs no capture). All other lanes (Linux x4, macOS x2, tooling x3) green on e2abce5.
Root cause (measured in the M0-TEST-01 CI, runs 34728950395/25 = runs
24/25, windows-2022 job): the runner never delivers handles the
process creates itself - fresh pipes, duplicated pipes, AND fresh
files, regardless of the INHERIT bit (confirmed set: raw=0x1) - to
child processes through STARTUPINFO. Only handles the process itself
inherited from its parent (kernel-assigned fd0/1/2) are delivered
(probe attempt 5). Every capture-pipe design therefore fails on this
runner; the fix stops depending on handle delivery at all.
Two-stage capture, ALL platforms (single code path, locally testable):
phase 1 (--run-a/--run-b, semantic change): spawn each scenario
binary WITHOUT redirecting its stdout - the child inherits
detcheck's own stdout (the CTest check script's execute_process
capture) - between tool-emitted marker lines
@@DETCHK-RUN-<A|B>-BEGIN@@ / @@DETCHK-RUN-<A|B>-END <exitcode>@@.
Phase 1 exits 0 when both children ran to completion, 2 on a
spawn or scenario failure (reason on stderr). It does not read
or compare the streams.
phase 2 (new --compare-combined=<file>): the check script writes
the captured combined stream to a file and re-runs detcheck,
which splits at the markers, re-runs the stream contract on each
run, compares, and emits the same report format and exit codes
(0/1/2) as before.
Changes:
- tools/detcheck/laige-detcheck.cpp: both old runScenario bodies
(CreatePipe/STARTUPINFO/PeekNamedPipe on Windows, fork/dup2 on
POSIX) replaced by spawnScenarioWithMarkers (plain inheritance,
WaitForSingleObject before GetExitCodeProcess on Windows - the
STILL_ACTIVE guard is preserved); new readFileBounded (64 KiB
reads, contract-capped) and extractRunStream; new mode 3
--compare-combined (mutually exclusive with modes 1/2); usage and
header docs updated.
- tests/detcheck/expect-detcheck-result.cmake.in: two-phase check
script (phase 1 execute_process, combined file, phase 2
--compare-combined; mode 1 stays single-shot; phase-1 diagnostics
preserved to <diag>.phase1 before phase 2 overwrites the file).
CMake-portability notes baked in: string(FIND)/string(LENGTH) take
a literal first argument (no variable expansion), and
get_filename_component's argument order differs between cmake
versions, so both are avoided.
- docs/api/detcheck.md: two-stage mechanism, report/exit-code
semantics, and the Windows runner handle-delivery finding
(DOC-007: same patch as the behavior change).
Provenance: this fixes the M0-TOOL-02 Windows path (its stale "Windows
path was compile-verified by the CI MSVC job" claim in the
roadmap/README.md changelog is corrected with the M0-TEST-01 entry);
the bug and fix land inside this PR per the established pattern.
Local: all 5 P0 trees (g++/shared/ASan/TSan/clang, Debug) 32/32
including the 8 detcheck tests on the new two-stage path; six-scenario
end-to-end smoke (identical/diverged/malformed/fail/stream-mismatch/
args) all correct. Windows branch is CI-verified (no MSVC locally).
Run 34732057356 (commit 08f332f) built clean and every non-spawn test passed, but all six mode-2 tests failed in phase 1: CreateProcessW failed for the scenario children. The difference from every spawn that DID succeed in that same process instance (the probe attempts, incl. the no-redirect control) is the STARTUPINFO argument: those pass a zeroed STARTUPINFOW (cb set, no dwFlags), the scenario spawn passed NULL - the runner's CreateProcess machinery rejects the NULL form. Pass a zeroed STARTUPINFOW (plain handle inheritance, identical semantics to NULL for the child) and carry the GetLastError() code in the failure message (CORE-008).
…ified Run 34746755055 (c8b8221) is fully green: all 10 jobs, Windows 32/32 with the two-stage marker capture working end-to-end on the windows-2022 runner, and the seed-identity pair is closed (e2abce5 -> c8b8221, byte-identical test-seed-check lines in the archived Linux ASan LastTest.log). Removed now that the diagnosis is complete: - laige-detcheck.cpp: probes 1-7, env dump, handle-bit dump, parent-process identification, the diag sink (g_diag/diagf/DiagFlush), LAIGE_DETCHECK_DIAG_FILE handling, the LAIGE_DETCHECK_CALLER tag, unused includes (<cstdarg>/<cerrno>/<cstring>). - probe-helper.cpp + the laige-detcheck-probe target. - expect-detcheck-result.cmake.in: the TEMP .phase1 diag save, the TEMP .check capture mirror, the TEMP passing-output print. The two-phase flow itself is the permanent design and stays. - tests/detcheck/CMakeLists.txt: DIAG_FILE/CALLER env wiring. - ci.yml + ci-pull.yml: the warm-up step (based on the disproven file-filter hypothesis) and the diag-cat loop. - roadmap: M0-TEST-01 Verify filled with the CI run ids and the byte-identical seed lines; M0-TEST-01 changelog row completed with the full Windows fix chain and provenance; M0-TOOL-02 row's stale 'Windows path was compile-verified' claim corrected (it was compile-only; the runtime path was fixed inside this PR).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements roadmap/M0-foundations.md step M0-TEST-01 (test infrastructure conventions), and — via the established provenance pattern — the Windows CI runtime fix for M0-TOOL-02's
laige-detcheckthat this step's verification exposed.What (M0-TEST-01)
docs/testing.md(new) — the normative conventions doc, linked fromdocs/README.md,tests/README.md,building.md,README.md:src/module,<module>_testsexecutables, per-suite CTest entries via unquoted--gtest_filter, GoogleTest dev-only (ADR 0004), non-module dirs (tests/tools,tests/api,tests/detcheck,tests/support,tests/testing).regress_<short-id>, must fail before the fix (AGENTS TEST-003).laige-fuzz: target registration, CI lane semantics (bounded--runs=1000in every P0 job's ctest = PRD §14 "every commit (bounded)"), nightly long-run form (--runs=1000000; scheduled lane lands with the first M1 fuzz target).tests/support/laige_test_seed.h—TestSeed()returns the fixed default0x1F055EED(identical to laige-fuzz'skDefaultSeed— one documented default seed repo-wide) unlessLAIGE_TEST_SEEDis set (0x-hex/decimal); a set-but-unparseable value records a loud test failure (CORE-008) and falls back to the default;TestPrng(id)derives a per-test substream from a stable named id.tests/testing/—test_infra_tests+ CTest entrytest_infra, suiteSeededRandom(6 cases): 65536-draw FNV-1a KATs under the default and override seeds, first-8-draw KAT, default/fuzz seed identity, the loud invalid-env path (EXPECT_NONFATAL_FAILUREfromgtest-spi.h), seed parser, substream isolation. Each KAT prints a machine-greppabletest-seed-checkline before asserting — the byte-identical-across-two-CI-runs identity check (the step's Verify clause).regress_test: M0-CORE-08'sConfigJsonValid.ObjectMemberWhitespacerenamed toConfigJsonValid.regress_json_object_member_ws(suite unchanged;ctest -R config_jsonstill covers it; historical M0-CORE-08 record unchanged).fuzz_json_parsectest entry in every P0 job).Windows CI fix chain (M0-TOOL-02 runtime, fixed in this PR)
The step's cross-run verification required the Windows CI lane to be trustworthy; observing it exposed two M0-TOOL-02 defects (the step record's "Windows path was compile-verified" claim was stale — corrected in
roadmap/README.md):@CHECKS@variable-name misspelling + CMake single-backslash string stripping eating the regex escapes) — fixed in7928379.STARTUPINFO; only parent-inherited (kernel-assigned) handles are delivered (measured runs 34728950395/34729337292). Additionally, NULLSTARTUPINFOis rejected by the runner's CreateProcess machinery (run 34732057356). Fixed by two-stage marker capture on all platforms (--run-a/--run-bphase 1 spawns each scenario child with plain stdout inheritance, delimited by@@DETCHK-RUN-A/B-BEGIN/END@@markers;--compare-combined=<file>phase 2 splits/validates/compares) + the zeroed-STARTUPINFOspawn (c8b8221). All diagnostics used for the diagnosis were stripped after the fix verified (d57bb79).Verified
build,build-shared,build-asan,build-tsan) and Clang 22.1.8 (build-clang);ctest -R test_infra,ctest -R fuzz_json_parsegreen in every tree;laige-fuzz json_parse --runs=1000clean.test-seed-checklines in the archived Linux ASanTesting/Temporary/LastTest.logacross runs 34713354925/34714039135 (same-commit pair) and again across commits e2abce5→c8b8221 (runs 34728782624/34746755055):d57bb79: dispatch run 34747610136 — all 10 jobs green, Windows 32/32; the push-triggered ci-pull run 34747612216 — green (macOS/Windows label-gated, skipped as designed).Notes
ENGINE-RULE-EXCEPTIONadded or retained.CreateProcessWbranch is CI-verified only (no MSVC/mingw locally); the two-stage flow is verified end-to-end on POSIX locally and on Windows in CI.