Files
codebase-memory-mcp/scripts/ci/test-impact-publish.sh
T
Martin Vogel 674a731fcc ci(test-impact): give the engine full history; publish maps with an incomplete suite
The test-impact engine certifies the history it diffs and refuses a shallow
clone ("Graft or shallow state prevents history certification"). The shadow
job, the gate-mode select-tests job and the main-branch producer all checked
out with fetch-depth 2. So every shadow answer since #2458 was run_all:
GIT_UNAVAILABLE, then POLICY_UNAVAILABLE, because the merge base's
.codebase-memory.json is read through the same git facts. That was 24 of 24
measured PR runs. All three jobs now check out full history (about 400 MiB,
these jobs only).

The producer never published a bundle either. coverage-map.py exits 1 when a
suite fails under coverage: the map is still written and that suite's tests
are incomplete. test-impact-publish.sh ran it under set -e, so the publish
aborted. The failing suite was test_impact_git. Its hostile-diff probe
re-enters the runner as a child, which inherited CBM_TEST_COVERAGE_DIR,
started a coverage run of its own, and refused the per-test LLVM_PROFILE_FILE
it inherits.

Two changes:
- The probe now clears CBM_TEST_COVERAGE_DIR, as it already clears
  CBM_TEST_ONLY_FILE. The child's profile then goes to the spawning test, like
  any other child process.
- publish treats exit 1 as a written, conservative map (incomplete tests run
  whole), with a warning that names the suite. It fails only on exit 2.

Proof:
- Depth-2 clone at f9ce3865: test-impact select answers run_all
  [GIT_UNAVAILABLE, POLICY_UNAVAILABLE] with "Graft or shallow state ...", as
  in CI. Same commit with full history: graph certified, run_all
  [RUNNER_REACHED] (the change reaches the runner's main).
- Coverage runner (Homebrew LLVM, macOS), test_impact_git: before, 43 passed
  and 2 failed ("a coverage run starts with LLVM_PROFILE_FILE=/dev/null");
  after, 45 passed. coverage-map.py --suite test_impact_git: 45 tests,
  0 incomplete, exit 0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-09 01:59:34 +02:00

125 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-impact-publish.sh — publish the team artifact bundle of the checked-out
# main commit (.github/workflows/test-impact-artifact.yml).
#
# Incremental (a push): the previous commit's bundle is the graph base and its
# coverage map the starting point; only the suites the push's own selection
# picks are re-run with coverage and merged in (scripts/test-impact/
# coverage-merge.py). The oldest observation is carried forward, so an
# incremental map ages out unless a full refresh runs.
# Full (weekly, manual, or no usable previous bundle): every suite is re-run
# and the graph is built from scratch. A weekly run ALSO builds the graph
# incrementally from the previous bundle and fails when the two graphs'
# content digests differ: an incremental index must equal a full one.
#
# Usage: scripts/ci/test-impact-publish.sh --binary PATH --runner PATH --out DIR
# --mode incremental|full [--before SHA] [--llvm-bin DIR] [--platform LABEL]
# Exit: 0 = published · 1 = failed · 2 = usage error.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
BINARY="" RUNNER="" OUT="" MODE="" BEFORE="" LLVM_BIN="" PLATFORM="linux-x86_64-clang21"
while [ $# -gt 0 ]; do
case "$1" in
--binary) BINARY="$2"; shift 2 ;;
--runner) RUNNER="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--mode) MODE="$2"; shift 2 ;;
--before) BEFORE="$2"; shift 2 ;;
--llvm-bin) LLVM_BIN="$2"; shift 2 ;;
--platform) PLATFORM="$2"; shift 2 ;;
*) sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2 ;;
esac
done
[ -n "$BINARY" ] && [ -n "$RUNNER" ] && [ -n "$OUT" ] || exit 2
case "$MODE" in incremental | full) ;; *) exit 2 ;; esac
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
# A private daemon rendezvous: the socket path must fit sun_path.
export CBM_RUNTIME_DIR="$(mktemp -d /tmp/cbm-rt.XXXXXX)"
export CBM_CACHE_DIR="$WORK/cache"
mkdir -p "$WORK/engine" && chmod 700 "$WORK/engine"
HEAD_SHA="$(git -C "$ROOT" rev-parse HEAD)"
PREVIOUS=""
if [ -n "$BEFORE" ]; then
if scripts/ci/test-impact-bundle.sh --fetch "$BEFORE" "$WORK/previous"; then
PREVIOUS="$WORK/previous"
fi
fi
if [ "$MODE" = incremental ] && [ -z "$PREVIOUS" ]; then
echo "test-impact-publish: no previous bundle for ${BEFORE:-?}: full refresh"
MODE=full
fi
map() { # out [suite...]
local out="$1"; shift
local args=()
local rc=0
for suite in "$@"; do args+=(--suite "$suite"); done
python3 "$ROOT/scripts/test-impact/coverage-map.py" --runner "$RUNNER" --out "$out" \
${LLVM_BIN:+--llvm-bin "$LLVM_BIN"} ${args[@]+"${args[@]}"} || rc=$?
# 1 = a suite failed or timed out under coverage: the map is still written
# and that suite's tests are `incomplete`, which only ever selects MORE
# tests (an incomplete test runs whole). Publish it, but say which suite.
# 2 = usage or toolchain error: no trustworthy map, fail.
if [ "$rc" -eq 1 ]; then
echo "::warning::coverage map: a suite failed under coverage; its tests stay incomplete ($(python3 -c 'import json,sys; print(", ".join(s["suite"] for s in json.load(open(sys.argv[1]))["suites"] if s["exit"] != 0))' "$out/meta.json"))"
return 0
fi
return "$rc"
}
OBSERVED=""
if [ "$MODE" = full ]; then
OBSERVED="$(date +%s)"
map "$WORK/coverage"
else
# The push's own selection decides what to re-run (whole suites).
"$BINARY" test-impact select --repo "$ROOT" --base "$BEFORE" --work "$WORK/engine" \
--artifact "$PREVIOUS" --artifact-commit "$BEFORE" --artifact-verified \
--platform "$PLATFORM" > "$WORK/selection.json"
python3 "$ROOT/scripts/test-impact/selection.py" suites "$WORK/selection.json" > "$WORK/suites.txt"
"$RUNNER" --list-suites > "$WORK/listed.txt"
if grep -qx '\*' "$WORK/suites.txt"; then
OBSERVED="$(date +%s)"
map "$WORK/coverage"
else
mapfile -t rerun < <(sort -u "$WORK/suites.txt")
map "$WORK/fresh" ${rerun[@]+"${rerun[@]}"}
python3 "$ROOT/scripts/test-impact/coverage-merge.py" --previous "$PREVIOUS/coverage" \
--fresh "$WORK/fresh" --suites "$WORK/listed.txt" --commit "$HEAD_SHA" \
--out "$WORK/coverage"
fi
fi
publish() { # out [previous]
"$BINARY" test-impact publish --repo "$ROOT" --out "$1" --work "$WORK/engine" \
--coverage "$WORK/coverage" ${OBSERVED:+--observed-at "$OBSERVED"} \
--platform "$PLATFORM" ${2:+--previous "$2"}
}
if [ "$MODE" = full ]; then
publish "$OUT"
if [ -n "$PREVIOUS" ]; then
publish "$WORK/incremental" "$PREVIOUS"
field() { python3 -c 'import json,sys;print(json.load(open(sys.argv[1]))[sys.argv[2]])' "$1" "$2"; }
# Topology (what selection reads) must be equal; edge properties and the
# corpus-statistical edges are reported, not gated (recorded defects).
full_topology="$(field "$OUT/receipt.json" graph_topology_sha256)"
incr_topology="$(field "$WORK/incremental/receipt.json" graph_topology_sha256)"
if [ "$full_topology" != "$incr_topology" ]; then
echo "test-impact-publish: incremental topology differs from the full one ($incr_topology != $full_topology)" >&2
exit 1
fi
if [ "$(field "$OUT/receipt.json" graph_content_sha256)" != \
"$(field "$WORK/incremental/receipt.json" graph_content_sha256)" ]; then
echo "::warning::incremental graph content (edge properties, similarity edges) differs from the full one; topology is equal"
fi
echo "test-impact-publish: incremental topology equals the full one ($full_topology)"
fi
else
publish "$OUT" "$PREVIOUS"
fi
echo "test-impact-publish: bundle of $HEAD_SHA in $OUT ($MODE)"