fix(test): make cd_boot_matrix build stamps converge across chunked runs

The matrix stamps every row it writes with scripts/build-id.sh's output and,
on resume, re-runs any row whose stamp differs from the current build. But
the default results file (docs/cd-boot-matrix.md) is tracked, so writing the
first row flipped the tree to "-dirty": the next invocation computed a
different id, judged all the rows it had just written stale, and re-ran them.
A chunked sweep (CD_MATRIX_MAX_RUNS) could therefore never settle, which is
the one thing chunking exists for. Observed as 6 rows stamped 5a83d3d
followed by "pass 1: 0/20 rows stamped 5a83d3d-dirty".

Exclude the results file from the dirty check in build-id.sh, which both
sides of the guard already share: gen-version-h.sh stamps the binary from the
same script, so the binary's embedded id and the harnesses' VJ_EXPECT_BUILD
stay in agreement. A source edit still yields "-dirty" and still invalidates
earlier rows, so the stale-row guard keeps doing its job.

Also warn when CD_MATRIX_OUT points at some other tracked file, which would
hit the same non-convergence without being covered by the ignore list.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Opus 5 committed 2026-07-30 23:16:21 -04:00
1 parent b8d4c7e90e
commit d8a7c793ee
2 files changed
+73 -4

No files matched your search

+38 -1
View File
@@ -5,13 +5,50 @@
# string reported by a loaded core (VJ_EXPECT_BUILD) so a stale or
# wrong-branch binary fails loudly instead of silently testing the
# wrong code. POSIX sh compatible -- no bashisms.
#
# `--ignores` prints the ignore list below, one path per line, so a caller
# can check whether the file it is about to write is covered.
set -e
ROOT=$(cd "$(dirname "$0")/.." && pwd)
# Tracked files that are RESULTS of running the test tools, never inputs to
# the build. Changes to these do not make the binary "dirty", because they
# cannot change a single byte of it.
#
# Why this list has to exist: a tool that records its results into a tracked
# file AND stamps those results with this build id cannot converge without
# it. test/tools/cd_boot_matrix.sh computes the id once per invocation and
# writes rows stamped with it into docs/cd-boot-matrix.md; writing the first
# row would flip the tree to "-dirty", so the next chunked invocation would
# compute a different id, judge every row it had just written to be stale
# evidence, and re-run all of them -- forever. Excluding the results file
# keeps the stamp meaningful (a real source edit still yields "-dirty")
# while letting a chunked sweep settle.
#
# Keep this minimal and results-only. Anything that feeds a compile, a link
# or a code-generation step must NOT be listed here: the whole point of the
# stamp is that it changes when the emulator code under test changes.
#
# Repo-root-relative paths, ONE PER LINE -- the list is fed to `grep -vxF`,
# which reads a newline-separated pattern set. Space-separating two entries
# makes a single literal pattern that matches no filename at all, silently
# restoring the non-convergence this list exists to prevent.
BUILD_ID_IGNORE='docs/cd-boot-matrix.md'
if [ "${1:-}" = "--ignores" ]; then
printf '%s\n' "$BUILD_ID_IGNORE"
exit 0
fi
REV=$(cd "$ROOT" && git rev-parse --short HEAD 2>/dev/null || echo unknown)
if [ "$REV" != "unknown" ]; then
if ! (cd "$ROOT" && git diff-index --quiet HEAD -- 2>/dev/null); then
# Name filtering rather than an ':(exclude)' pathspec: it needs no
# pathspec-magic support, and a malformed entry can only ever fail
# towards "-dirty" (the safe direction) instead of silently matching
# everything and disabling the guard.
if (cd "$ROOT" && git diff-index --name-only HEAD -- 2>/dev/null) \
| grep -vxF "$BUILD_ID_IGNORE" | grep -q .; then
REV="${REV}-dirty"
fi
fi
+35 -3
View File
@@ -52,7 +52,11 @@
# mktemp -d; NOT committed -- path is printed at the end)
# CD_MATRIX_MAX_RUNS max NEW title x mode runs per invocation (default 0 =
# unlimited). Combined with the resume guard this lets
# a long sweep be chunked across short invocations.
# a long sweep be chunked across short invocations:
# each chunk skips the rows earlier chunks recorded,
# because the build stamp does not move when the only
# thing that changed is this script's own output
# (scripts/build-id.sh BUILD_ID_IGNORE).
set -u
# ---------------------------------------------------------------------------
@@ -68,6 +72,7 @@ FRAMES="${CD_MATRIX_FRAMES:-3000}"
TIMEOUT_SECS="${CD_MATRIX_TIMEOUT:-120}"
OUT="${CD_MATRIX_OUT:-docs/cd-boot-matrix.md}"
LOGDIR="${CD_MATRIX_LOGDIR:-$(mktemp -d "${TMPDIR:-/tmp}/cd_boot_matrix.XXXXXX")}"
MAX_RUNS="${CD_MATRIX_MAX_RUNS:-0}" # 0 = unlimited; see the resume guard below
mkdir -p "$LOGDIR"
@@ -107,6 +112,25 @@ fi
# Build-identity guard: every harness invocation below verifies that the
# core's embedded git rev matches the working tree (scripts/build-id.sh),
# so matrix rows can never be produced by a stale/wrong-branch binary.
#
# The id is computed ONCE per invocation and stamped into every row this
# invocation writes (see run_title). Resuming across invocations only
# works because scripts/build-id.sh excludes the results file from its
# dirty check -- otherwise writing the first row would flip the id and the
# next invocation would re-run everything it had just recorded. The
# default $OUT is on that ignore list; a custom tracked $OUT is not, so
# warn rather than let the sweep silently fail to converge. (This applies
# to any re-invocation, chunked or not: an interrupted unlimited sweep
# resumes through the same guard.)
if git ls-files --error-unmatch "$OUT" >/dev/null 2>&1 \
&& ! scripts/build-id.sh --ignores 2>/dev/null | grep -qxF "$OUT"; then
echo "WARNING: CD_MATRIX_OUT=$OUT is tracked by git but is not in" >&2
echo " scripts/build-id.sh's ignore list. Writing rows will flip the build" >&2
echo " id to '-dirty', so the next chunked invocation will treat every row" >&2
echo " this one records as stale and re-run it. Add the path to" >&2
echo " BUILD_ID_IGNORE, or point CD_MATRIX_OUT at an untracked file." >&2
fi
VJ_EXPECT_BUILD=$(scripts/build-id.sh 2>/dev/null || true)
export VJ_EXPECT_BUILD
if [ -n "$VJ_EXPECT_BUILD" ]; then
@@ -352,7 +376,12 @@ echo "=== CD boot matrix: $FRAMES frames/title/mode, ${TIMEOUT_SECS}s wall-clock
# every actual harness run of that title passed). Row matching is also
# scoped to the primary results table, so rows quoted in prose notes can
# never satisfy the resume guard.
MAX_RUNS="${CD_MATRIX_MAX_RUNS:-0}" # 0 = unlimited
#
# The stamp stays stable across the chunks of one sweep because
# scripts/build-id.sh does not count writes to this results file as a
# source change (its BUILD_ID_IGNORE list); a real edit to emulator code
# still flips it to "-dirty" and correctly invalidates earlier rows.
# MAX_RUNS is set with the other env knobs at the top of the script.
RUNS_DONE=0
# Write the header only on a fresh file; strip any previous footer so
@@ -391,7 +420,10 @@ else
printf 'recorded **by the same core build** (each row carries a hidden\n'
printf '`<!-- build:<rev> -->` stamp) are skipped, so an interrupted sweep resumes\n'
printf 'where it left off; rows recorded by a different build, or unstamped legacy\n'
printf 'rows, are re-run and replaced in place rather than silently reused. For\n'
printf 'rows, are re-run and replaced in place rather than silently reused.\n'
printf 'Recording rows does not itself move the stamp -- `scripts/build-id.sh`\n'
printf 'excludes this results file from its dirty check -- so the chunks of one\n'
printf 'sweep share a stamp and each chunk resumes where the last stopped. For\n'
printf 'long sweeps (or agent sessions with per-command time limits), run chunked:\n\n'
printf '```bash\n'
printf '# ~3 runs x <=120s wall each per invocation; repeat until every line\n'