Skip to content

test(runner): record portable suite observations - #46

Open
johnny4young wants to merge 3 commits into
mainfrom
codex/maintenance-g2
Open

johnny4young wants to merge 3 commits into
mainfrom
codex/maintenance-g2

Conversation

@johnny4young

@johnny4young johnny4young commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

The portable runner executes fixed, Bash 3.2-compatible waves, but it records no per-suite timing evidence. This PR adds an opt-in --summary PATH JSON report and has CI keep one per interpreter. The scheduler and the pass criteria are unchanged.

What changed

  • scripts/run-tests.bash --summary PATH writes:
    • per suite: path, passed / failed / skipped (OS rule), whole-second duration, child exit status and reason;
    • per run: target and host OS, $BASH_VERSION, source HEAD (null for exported trees), dirty and jobs.
  • The file is written atomically (mktemp + mv) even when a child fails. Missing or corrupt status, duration or log evidence fails closed and is recorded with a reason.
  • JSON strings are quoted with Bash builtins only (no jq/Python), byte-wise under LC_ALL=C.
  • Discovery uses git ls-files -z, so quoted non-ASCII paths are handled. Exported tarballs fall back to printf '%s\0' tests/*.bash.
  • CI writes test-summary-current.json and, on macOS, test-summary-bash32.json, and uploads them with if: always() as suite-observations-<os>.
  • CONTRIBUTING.md, CHANGELOG.md, and new invariants in tests/runner.bash and tests/workflows.bash.

Why

Before proposing any change to the wave scheduler, we need matching per-suite observations, from the same source, fixtures, runner and interpreter class. These are coarse measurements, not a claimed saving.

How it was tested

  • Runner fixtures cover:
    • serial and parallel outcomes, all selected logs after child failures, signals and literal names;
    • quoted and backslash JSON strings, timings and duplicate requests;
    • exported sources, missing or corrupt markers, and summary write failures;
    • (new) relative summary paths.
  • Independent review checks:
    • json_string round-trips UTF-8 (café, 日本), ", \, TAB and \x01 exactly through Python's json.loads, under both C and C.UTF-8;
    • the summary arrays are only written in the parent shell.
  • Full suite on head 386ddb2, as a non-root user, with --summary: 31/31 passed. The generated JSON parsed, with head 386ddb2, dirty: false and 31 suites.
  • ShellCheck 0.11.0, shfmt 3.13.1 (-i 2 -ci -bn), bash -n and tests/workflows.bash are clean.
  • Hosted CI on 386ddb2: all 10 jobs and all 11 checks green, including native macOS Bash 3.2 and Windows Bash/PowerShell. Previous qualification: run 37375586671 on 4f03846.

Review follow-ups

  • 386ddb2 fix(runner): resolve a relative --summary path against the caller. The runner cds to the repository root before writing, so --summary out.json used to land in the checkout (as an untracked file) instead of next to the caller. A relative path is now anchored to $PWD before the cd; /… and Windows C:/… / C:\… paths are kept as given. The new regression in tests/runner.bash fails without the fix.

Decisions taken

  • Durations stay at whole-second SECONDS resolution, to stay portable to Bash 3.2. No sub-second clock dependency was added.

Known limitations & follow-ups

  • dirty uses git diff --quiet HEAD --, which ignores untracked files. Untracked suites can't run anyway (discovery uses ls-files), but untracked fixtures could still change a result.
  • No scheduler change, threshold relaxation or quantified CI saving is claimed.

Merge order

#46 → #47, then #45. #47 is stacked on this branch: merge this first, then retarget #47 to main.

The runner changes into the repository root before writing the summary,
so a relative --summary path landed inside the checkout (leaving an
untracked file) instead of beside the caller. Anchor it to the caller's
directory first, keeping absolute POSIX and Windows drive paths as is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExmnfUngrDYjAheGvYwMnn

@johnny4young johnny4young left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: runner records portable suite observations

What it does: scripts/run-tests.bash --summary PATH writes an opt-in JSON report. Per suite it records status, whole-second duration, exit status and reason, plus host/target OS, Bash version and HEAD/dirty. Quoting is pure Bash. The file is written atomically via mktemp + mv, and missing or corrupt status/duration/log evidence fails closed. Discovery switches to git ls-files -z. CI writes one summary per interpreter and uploads them with if: always().

Verdict: ready after the fix I pushed.

Fixed (pushed 386ddb2)

  • 🟠 scripts/run-tests.bash: the runner cds to the repository root before writing the summary. A relative --summary path was therefore resolved against the repo, not the caller. Repro: cd /tmp/x && …/scripts/run-tests.bash --summary out.json changelog wrote <repo>/out.json and left an untracked file in the checkout (which then also flips "dirty" only if tracked, so it silently pollutes git status). The fix anchors a relative path to $PWD before the cd and leaves /… and Windows C:/… / C:\… paths alone. A regression test was added in tests/runner.bash; it fails without the fix.
    • Validation: full scripts/run-tests.bash --jobs 4 --summary … as a non-root user gave 31/31 passed. ShellCheck 0.11.0, shfmt 3.13.1 (-i 2 -ci -bn), bash -n and tests/workflows.bash were clean. #47 still merges cleanly on top, and its tests/runner.bash passes.

Checked and fine

  • json_string: verified with UTF-8 (café, 日本), ", \, TAB and \x01 under both C and C.UTF-8. Python's json.loads round-trips it exactly. local LC_ALL=C makes the walk byte-wise, so multibyte sequences are re-emitted intact.
  • record_suite runs only in the parent shell (skip loop over <<<"$selected", and report_suite after wait), so the arrays aren't lost in subshells. SECONDS works in the backgrounded run_suite subshells. "jobs":%s is always a validated integer by the time it is printed.
  • git ls-files -z + read -d '' is bash 3.2-safe and fixes quoted non-ASCII paths. The printf '%s\0' tests/*.bash fallback keeps exported tarballs working.

Nits

  • 🟡 write_summary: dirty uses git diff --quiet HEAD --, which ignores untracked files. Untracked suites can't run (discovery is ls-files), but untracked fixtures or helpers can still change results. git status --porcelain --untracked-files=normal would be stricter if this field is meant for comparing runs.
  • 🟡 Each json_string call forks nothing but loops per character in Bash, which is fine at the current size. Just don't reuse it for log bodies.
  • 🟡 This PR and #45 both append to the same spots in CHANGELOG.md / CONTRIBUTING.md, so the second to merge needs a trivial conflict resolution.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants