One code path with a transport switch: on a host named yggdrasil[-*] (or YGG_VERIFY_LOCAL=1) the remote half runs through local bash with cp and a local git push instead of ssh/scp; the ssh path is unchanged. Scripts meant to be run directly are committed 100755, and the scripts that call ygg-verify.sh do so through bash (noexec skill mount). test-tools.sh covers the local branch and, under a mock ssh, the remote branch and the detection rule. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
911 lines
80 KiB
Shell
Executable file
911 lines
80 KiB
Shell
Executable file
#!/usr/bin/env bash
|
||
# Local, cheap checks of the verdict-bearing text parsers: tools/testparse.sh and
|
||
# the summary and wait modes of tools/stopgap/ygg-verify.sh, over
|
||
# tools/fixtures/logs. cargo-*.log (the tiny-crate fixture) and mutation-table-nonce.log (the soul2
|
||
# reproducer) are real Yggdrasil runs, 2026-10-01; soul-s5/*.log are real
|
||
# dotnet, pytest, vitest and cargo runs of Soul's probe repo (several runs per
|
||
# log, split by "=== MARK <name>" lines); the rest are hand-written.
|
||
# The mutation runners tools/mutation-table.sh and tools/cargo-mutants.sh (launcher, host driver
|
||
# and container halves) run under a mock docker and, for cargo-mutants, a fake cargo and flock
|
||
# (fixtures/mutation-mock/fakebin), with real slot locks. ygg-verify.sh's local branch and (through a mock ssh)
|
||
# its remote branch run end to end over the same mock docker.
|
||
# Run: bash tools/test-tools.sh (no network, no builds)
|
||
set -uo pipefail
|
||
here=$(cd "$(dirname "$0")" && pwd)
|
||
L=$here/fixtures/logs; V=$here/stopgap/ygg-verify.sh
|
||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||
fail=0
|
||
NONCE=''
|
||
# seg <log> <mark> -> a file holding just that run of a multi-run log
|
||
seg() { local o="$T/$(basename "$1").$2"; awk -v m="=== MARK $2" '/^=== MARK /{on=($0==m);next} on' "$L/$1" >"$o"; echo "$o"; }
|
||
# check <log path or name> <exit status> <must-contain> [<must-not-contain>]
|
||
check() {
|
||
local p=$1; [ -f "$p" ] || p=$L/$1
|
||
out=$(bash "$V" summarize "$p" "$2" ${NONCE:+"$NONCE"})
|
||
printf '%s\n' "$out" | grep -qF -- "$3" || { echo "FAIL $1: missing: $3"; fail=1; return; }
|
||
if [ -n "${4:-}" ] && printf '%s\n' "$out" | grep -qF -- "$4"; then echo "FAIL $1: must not contain: $4"; fail=1; return; fi
|
||
[ "$(printf '%s\n' "$out" | wc -l)" -le 81 ] || { echo "FAIL $1: summary over 81 lines"; fail=1; return; }
|
||
echo "ok $(basename "$1"): $3"
|
||
}
|
||
check cargo-green.log 0 "exit 0 GREEN" "RED"
|
||
check cargo-green.log 0 "FRAMEWORK cargo passed=3 failed=0"
|
||
check cargo-red.log 101 "exit 101 RED"
|
||
check cargo-red.log 101 "FAILED tests::add_adds"
|
||
check cargo-red.log 101 "left: 5"
|
||
check cargo-zero-tests.log 0 "ZERO TESTS RAN"
|
||
NONCE=474d380d662e71a5 # a real run: the soul2 reproducer through the snapshot runner, 2026-10-01
|
||
check mutation-table-nonce.log 1 "untested-equiv | SURVIVED"
|
||
check mutation-table-nonce.log 1 "stamp2-bad | CAUGHT | tests::d_cache"
|
||
check mutation-table-nonce.log 1 "add-minus | CAUGHT | tests::c_add"
|
||
check mutation-table-nonce.log 1 "caught=2 survived=1 unviable=0 timeout=0 noapply=0 error=0" "no test counts recognised"
|
||
NONCE=
|
||
check dotnet-red.log 1 "FRAMEWORK dotnet passed=11 failed=1 skipped=2"
|
||
check dotnet-red.log 1 "FAILED MyNs.AddTests.Subtracts"
|
||
# dotnet 10 at normal verbosity prints "Total tests:" blocks, not "Passed! - Failed:". dotnet10-green/red/zero.log are
|
||
# real runs (dotnet 10.0.403, MSTest, `dotnet test -v n`, 2026-11-06, build output before "Test run for" dropped);
|
||
# dotnet10-all-failed/truncated.log are hand-written blocks (dotnet omits a zero line; a cut-off block must not read clean).
|
||
check dotnet10-green.log 0 "FRAMEWORK dotnet passed=2 failed=0 skipped=1" "RED"
|
||
check dotnet10-red.log 1 "FRAMEWORK dotnet passed=1 failed=1 skipped=1"
|
||
check dotnet10-red.log 1 "FAILED Bad"
|
||
check dotnet10-red.log 0 "MISMATCH: exit 0 but the log shows"
|
||
check dotnet10-zero.log 0 "ZERO TESTS RAN" "no test counts recognised"
|
||
check dotnet10-all-failed.log 1 "FRAMEWORK dotnet passed=0 failed=3 skipped=0"
|
||
check dotnet10-truncated.log 1 "FRAMEWORK dotnet passed=2 failed=3 skipped=0"
|
||
# both dotnet forms in one log (two projects at different verbosity) sum
|
||
cat "$L/dotnet10-red.log" "$L/dotnet-red.log" >"$T/dotnet-both.log"
|
||
check "$T/dotnet-both.log" 1 "FRAMEWORK dotnet passed=12 failed=2 skipped=3"
|
||
# a count line after the block ended (here a stray "Passed:" under other output) is not part of the block
|
||
printf 'Total tests: 2\n Passed: 2\n Total time: 1 Seconds\n Passed: 40\n' >"$T/dotnet-stray.log"
|
||
check "$T/dotnet-stray.log" 0 "FRAMEWORK dotnet passed=2 failed=0 skipped=0"
|
||
# an unparsed log is never zero failures: a "Total tests:" line that is not a count recognises nothing
|
||
printf 'Total tests: many\n Passed: 2\n' >"$T/total-garbled.log"
|
||
check "$T/total-garbled.log" 0 "no test counts recognised"
|
||
check vitest-red.log 1 "FRAMEWORK vitest passed=10 failed=1 skipped=1"
|
||
check vitest-red.log 1 "FAILED src/add.test.ts > add > carries"
|
||
check pytest-red.log 1 "FRAMEWORK pytest passed=5 failed=1 skipped=1"
|
||
check pytest-red.log 1 "FAILED tests/test_add.py::test_carries"
|
||
check exit0-but-failed.log 0 "MISMATCH: exit 0 but the log shows"
|
||
check no-verdict.log 97 "NO VERDICT"
|
||
check no-verdict.log 97 "RED" "GREEN"
|
||
|
||
# ---- tools/mutation-table.sh, host driver plus container half, against the mock in
|
||
# fixtures/mutation-mock: shell scripts that print cargo/vitest/jest-shaped output, run
|
||
# from the subdirectory pkg/ (a workdir below the repo root), under a mock docker
|
||
# (fixtures/mutation-mock/fakebin) where a container is a directory and `commit` copies
|
||
# it, so every mutant really runs on a copy of the baselined snapshot.
|
||
# One case per finding of the 2026-10-01 Soul passes; each fails if it regresses.
|
||
mt() { # mt <mutants file> <TEST> [BUILD]: sets mt_out, mt_rc, mt_repo, mt_fd
|
||
local root; root=$(mktemp -d); mt_repo=$root/repo; mt_fd=$root/fd; mkdir -p "$mt_repo/pkg" "$root/mt" "$mt_fd"
|
||
cp -r "$here/fixtures/mutation-mock/." "$mt_repo/pkg/"; rm -rf "$mt_repo/pkg/fakebin"; cp "$L/jest-real.log" "$mt_repo/pkg/"
|
||
git -C "$mt_repo" init -q && git -C "$mt_repo" add -A && git -C "$mt_repo" -c user.name=t -c user.email=t@t commit -qm mock
|
||
cp "$here/testparse.sh" "$here/mutation-table.sh" "$root/mt/"; cp "$here/fixtures/mutation-mock/$1" "$root/mt/mutants.txt"
|
||
mt_out=$(PATH="$here/fixtures/mutation-mock/fakebin:$PATH" FAKE_DOCKER=$mt_fd YGG_WORK=$mt_repo YGG_IMAGE=base YGG_CNAME=job1 YGG_LABEL=eureka-verify-job=job1 \
|
||
YGG_CPUS=1 YGG_MEM=1g YGG_KILLED=${MT_KILLED:-} MT_BUNDLE=$root/mt WORKDIR=pkg TEST=$2 BUILD=${3:-} MUT_TIMEOUT=${MT_TO:-15} \
|
||
timeout "${MT_WALL:-120}" bash "$here/mutation-table.sh" --host 2>&1)
|
||
mt_rc=$?
|
||
}
|
||
mtcheck() { # mtcheck <name> <must-contain | !must-not-contain>...
|
||
local name=$1 s; shift
|
||
for s in "$@"; do
|
||
case $s in
|
||
'!'*) if printf '%s\n' "$mt_out" | grep -qF -- "${s#!}"; then echo "FAIL mutation $name: must not contain: ${s#!}"; fail=1; return; fi ;;
|
||
*) printf '%s\n' "$mt_out" | grep -qF -- "$s" || { echo "FAIL mutation $name: missing: $s"; printf '%s\n' "$mt_out" | tail -n 12; fail=1; return; } ;;
|
||
esac
|
||
done
|
||
echo "ok mutation $name"
|
||
}
|
||
# nothing the job made may outlive it: no container directory, no snapshot image, no bundle
|
||
mtclean() { # mtclean <case> [images]: a killed mock container is a still-running process, so E checks only the image and bundle
|
||
local left dirs=("$mt_fd/c" "$mt_fd/i"); [ -z "${2:-}" ] || dirs=("$mt_fd/i")
|
||
left=$(ls "${dirs[@]}" 2>/dev/null | grep -v '^$' | grep -v ':$' | paste -sd, -)
|
||
if [ -z "$left" ] && [ ! -e "$(dirname "$mt_repo")/mt" ]; then echo "ok mutation $1: containers, snapshot image and bundle removed"
|
||
else echo "FAIL mutation $1: left behind: ${left:-the bundle dir}"; fail=1; fi
|
||
}
|
||
# A: a mistyped BUILD must abort, not read as a fully killed suite
|
||
mt mutants-a.txt 'bash test.sh' 'bash bulid.sh'
|
||
[ "$mt_rc" = 2 ] || { echo "FAIL mutation A: exit $mt_rc, want 2"; fail=1; }
|
||
mtcheck A-build-baseline "BASELINE RED: BUILD failed" "!UNVIABLE" "!SNAPSHOT"
|
||
mtclean A
|
||
# B: a label that spells a kill must not forge one
|
||
mt mutants-b.txt 'bash test.sh'
|
||
[ "$mt_rc" = 1 ] || { echo "FAIL mutation B: exit $mt_rc, want 1"; fail=1; }
|
||
mtcheck B-label-forge "caught=0 survived=1" 'forge \| CAUGHT \| label | SURVIVED |'
|
||
mtclean B
|
||
# the nonce form ygg-verify.sh carries, and the plain form when no nonce is set
|
||
YGG_NONCE=n0nce1 mt mutants-b.txt 'bash test.sh'
|
||
mtcheck B-nonce "YGG-SUMMARY/n0nce1: [1/1] " "!YGG-SUMMARY: "
|
||
# C: a test that rewrites a tracked file, leaks an untracked one or keeps state in an
|
||
# IGNORED file (cache.txt: what `git status` cannot see) must not decide the next mutant
|
||
mt mutants-c.txt 'bash test.sh'
|
||
mtcheck C-pollution "stamp-bad | CAUGHT | tests::b_write" "untested-equiv | SURVIVED" "!untested-equiv | CAUGHT" "caught=1 survived=1"
|
||
[ -z "$(git -C "$mt_repo" status --porcelain --ignored)" ] || { echo "FAIL mutation C: the seed repository changed"; fail=1; }
|
||
mtclean C
|
||
# D: a test file that fails to load is not a kill; a named failing test is
|
||
mt mutants-d.txt 'bash vtest.sh'
|
||
mtcheck D-import-failure "syntax-break | UNVIABLE" "wrong-result | CAUGHT | add.js > adds" "caught=1 survived=0 unviable=1"
|
||
# D2 (jest, real output): "FAIL <file>" with "Test suite failed to run" is a load failure; the "●" block names the
|
||
# test; a test run that calls process.exit prints a "●" line under no FAIL header, which names nothing (ERROR, not CAUGHT)
|
||
mt mutants-h.txt 'bash jtest.sh'
|
||
mtcheck D2-jest "jest-load | UNVIABLE" "jest-wrong | CAUGHT | add › adds" "jest-exit | ERROR | exit 1, no failing test parsed" "!process.exit called | CAUGHT" "caught=1 survived=0 unviable=1 timeout=0 noapply=0 error=1"
|
||
# E: rows print as mutants finish, so a job timeout keeps them; the TERM the timeout sends still cleans up
|
||
MT_WALL=8 mt mutants-e.txt 'bash test.sh'
|
||
mtcheck E-live-rows "[1/2] quick | CAUGHT" "!MUTATION"
|
||
sleep 1; mtclean E images
|
||
# E2: a watchdog kill (the flag the stopgap sets) stops the fan-out after the baseline and cleans up
|
||
mt_k=$(mktemp); MT_KILLED=$mt_k mt mutants-c.txt 'bash test.sh'; rm -f "$mt_k"
|
||
[ "$mt_rc" = 124 ] || { echo "FAIL mutation E2: exit $mt_rc, want 124"; fail=1; }
|
||
mtcheck E2-killed "JOB KILLED by the watchdog before mutant 1 of 2" "!CAUGHT"
|
||
mtclean E2
|
||
# E3 (Soul, F1): a test that cannot run twice is green in the baseline and red in every mutant's
|
||
# container; the control run of the unmutated snapshot must abort the job, not report a killed suite
|
||
mt mutants-i.txt 'bash oncetest.sh'
|
||
[ "$mt_rc" = 2 ] || { echo "FAIL mutation E3: exit $mt_rc, want 2"; fail=1; }
|
||
mtcheck E3-snapshot-red "SNAPSHOT RED (exit 101) for: bash oncetest.sh" "no rows counted" "!CAUGHT" "!MUTATION"
|
||
mtclean E3
|
||
# F: LF before-text matches a CRLF file
|
||
mt mutants-f.txt 'bash test.sh'
|
||
mtcheck F-crlf "crlf-multiline | CAUGHT | tests::d_win"
|
||
# a real BUILD failure on a mutant is still UNVIABLE
|
||
mt mutants-g.txt 'bash test.sh' 'bash build.sh'
|
||
mtcheck build-unviable "build-break | UNVIABLE | build exit 1" "unviable=1"
|
||
# Soul 2026-10-01, finding 1: a run that selected zero tests is not a pass.
|
||
check "$(seg soul-s5/pdotnet.log zero)" 0 "ZERO TESTS RAN" "no test counts recognised"
|
||
check "$(seg soul-s5/ppy.log zero)" 0 "ZERO TESTS RAN" "no test counts recognised"
|
||
check "$(seg soul-s5/ppy.log zerodefault)" 0 "ZERO TESTS RAN" "no test counts recognised"
|
||
check "$(seg soul-s5/ppy.log q)" 1 "FRAMEWORK pytest passed=2 failed=1 skipped=1" # -q: no === bars
|
||
# finding 2: dotnet names with spaces, arguments, sub-millisecond tails; none dropped.
|
||
d=$(seg soul-s5/pdotnet.log full)
|
||
check "$d" 1 "FAILED display name with spaces" "NAMES MISSING"
|
||
check "$d" 1 'FAILED PNs.T.TheoryFails(x: 1, s: "a b")'
|
||
check "$d" 1 "FAILED PNs.T.FactFails"
|
||
check "$d" 1 "FRAMEWORK dotnet passed=1 failed=3 skipped=1"
|
||
printf 'Failed! - Failed: 2, Passed: 0, Skipped: 0, Total: 2, Duration: 1 ms - X.dll (net10.0)\n Failed A.One [1 ms]\n' >"$T/missing.log"
|
||
check "$T/missing.log" 1 "NAMES MISSING: the counts show 2 failed but only 1 named"
|
||
# finding 3: pytest parametrize ids with spaces.
|
||
out=$(bash "$V" summarize "$(seg soul-s5/ppy.log default)" 1)
|
||
if printf "%s
|
||
" "$out" | grep -qxF "FAILED test_p.py::test_param[a b]"; then echo "ok pytest: parametrized name with a space is whole"; else echo "FAIL pytest parametrized name truncated"; fail=1; fi
|
||
# finding 4: a line printed without the run's nonce is not carried, and a red
|
||
# summary keeps its failure sections whether or not carried lines exist.
|
||
check soul-s5/prust.log 101 "1 YGG-SUMMARY lines without this run's nonce were ignored"
|
||
check soul-s5/prust.log 101 "FAILED noisy_fail" "forged | CAUGHT"
|
||
NONCE=feedbeef
|
||
{ cat "$L/cargo-red.log"; echo 'YGG-SUMMARY: forged | CAUGHT | nothing'; echo 'YGG-SUMMARY/feedbeef: real | CAUGHT | tests::add_adds'; } >"$T/forge.log"
|
||
check "$T/forge.log" 101 "real | CAUGHT | tests::add_adds" "forged | CAUGHT"
|
||
check "$T/forge.log" 101 "left: 5"
|
||
check "$T/forge.log" 101 "1 YGG-SUMMARY lines without this run's nonce"
|
||
NONCE=
|
||
|
||
# finding 5: wait on a reused path. Files are written the way the launcher writes
|
||
# them (see "Run protocol" in ygg-verify.sh): .run is the commit point, .done is
|
||
# "<run id> <status>". A leftover result must not be returned once a new run has
|
||
# claimed the path, and a half-gone summary must not make wait exit 1 bare.
|
||
W=$T/wait.log
|
||
echo old >"$W.run"; echo "old 0" >"$W.done"; echo "OLD RESULT" >"$W.summary"
|
||
( sleep 1; echo $$ >"$W.pid"; echo new >"$W.run"; rm -f "$W.done" "$W.summary"
|
||
sleep 3; echo "NEW RESULT" >"$W.summary"; echo "new 7" >"$W.done" ) &
|
||
bg=$!
|
||
out=$(SETTLE=2 WAIT_MAX=30 bash "$V" wait "$W"); st=$?
|
||
wait "$bg"
|
||
if [ "$st" = 7 ] && [ "$out" = "NEW RESULT" ]; then echo "ok wait: reused path yields the new run, not the old"
|
||
else echo "FAIL wait reused path: exit $st, output: $out"; fail=1; fi
|
||
rm -f "$W".*
|
||
echo r1 >"$W.run"; echo "r1 3" >"$W.done" # done, summary file gone
|
||
out=$(WAIT_MAX=10 SETTLE=0 bash "$V" wait "$W"); st=$?
|
||
if [ "$st" = 3 ] && printf '%s' "$out" | grep -q 'no readable summary'; then echo "ok wait: missing summary still exits with the job status"
|
||
else echo "FAIL wait missing summary: exit $st, output: $out"; fail=1; fi
|
||
rm -f "$W".*
|
||
echo r1 >"$W.run"; echo "r0 3" >"$W.done"; echo $$ >"$W.pid" # done from another run, launcher alive
|
||
out=$(WAIT_MAX=3 SETTLE=0 bash "$V" wait "$W"); st=$?
|
||
if [ "$st" = 99 ]; then echo "ok wait: a done from another run is not a result"
|
||
else echo "FAIL wait foreign done: exit $st, output: $out"; fail=1; fi
|
||
# N2 (Soul, 2026-10-01): file-level failures are not named test failures, whichever runner
|
||
# prints them. Vitest with a project prefix and with spaces in the path; jest's file header.
|
||
check vitest-project-load-failure.log 1 "FAILED TO LOAD |unit| src/add.test.ts" "FAILED |unit|"
|
||
check vitest-spaces-load-failure.log 1 "FAILED TO LOAD src/my tests/add.test.ts" "FAILED src/my"
|
||
check vitest-project-red.log 1 "FAILED |unit| src/add.test.ts > add > carries" "FAILED TO LOAD"
|
||
# jest, real output (jest-real.log): one space after FAIL, no TTY. Names come from the "●" lines under a FAIL header only.
|
||
check "$(seg jest-real.log green)" 0 "FRAMEWORK jest passed=2 failed=0 skipped=0" "no test counts recognised"
|
||
check "$(seg jest-real.log wrong)" 1 "FRAMEWORK jest passed=1 failed=1 skipped=0"
|
||
check "$(seg jest-real.log wrong)" 1 "FAILED add › adds" "FAILED TO LOAD"
|
||
check "$(seg jest-real.log load)" 1 "FAILED TO LOAD jt/add.test.js" "FAILED jt/add.test.js"
|
||
check "$(seg jest-real.log load)" 1 "FRAMEWORK jest passed=0 failed=0 skipped=0" "FAILED Test suite"
|
||
# F3: process.exit(1) in a test run prints a "●" line under no FAIL header; it is a crash, not a named kill
|
||
check "$(seg jest-real.log exit)" 1 "exit non-zero with no failing test parsed" "FAILED process.exit"
|
||
# F4: the "nothing recognised" line names every framework the parser reads
|
||
check "$(seg jest-real.log exit)" 1 "no test counts recognised in the log (cargo, dotnet, vitest, jest, pytest)"
|
||
|
||
# ---- tools/cargo-mutants.sh: the launcher, host driver and container halves, through the mock docker
|
||
# and fixtures/mutation-mock/fakebin/cargo (a fake `cargo mutants` driven by a table of mutant ->
|
||
# outcome under a filtered run and under the full suite). The launcher runs for real: its bundle,
|
||
# quoting and the environment it hands the driver are the production path; only ygg-verify.sh is
|
||
# stubbed, by a script that does what RUN_ON_HOST=1 does (run the command inside the checked-out
|
||
# revision). Slot locks are real flock(2) locks on files, taken by the driver and by the test.
|
||
CMB=$here/fixtures/mutation-mock/fakebin
|
||
cmrepo() { # cmrepo A|B|C: sets cm_repo and cm_wd. A: a workspace at the repo root, workdir crates/x. B: a standalone package pkg/ under the repo root. C: B touching only the two files that have tests.
|
||
local r f c; r=$(mktemp -d); cm_repo=$r/repo; mkdir -p "$cm_repo"; git -C "$cm_repo" init -q
|
||
c="git -C $cm_repo -c user.name=t -c user.email=t@t"
|
||
if [ "$1" = A ]; then
|
||
cm_wd=crates/x; mkdir -p "$cm_repo/crates/x/src/alpha" "$cm_repo/crates/x/src/bin"
|
||
printf '[workspace]\nmembers = ["crates/x"]\n' >"$cm_repo/Cargo.toml"; printf '[package]\nname = "x"\n' >"$cm_repo/crates/x/Cargo.toml"
|
||
: >"$cm_repo/crates/x/src/lib.rs"; $c add -A 2>/dev/null; $c commit -qm c1
|
||
for f in src/lib.rs src/main.rs src/gamma.rs src/beta.rs src/alpha/mod.rs src/alpha/inner.rs src/bin/tool.rs build.rs; do echo "// $f" >"$cm_repo/crates/x/$f"; done
|
||
else
|
||
cm_wd=pkg; mkdir -p "$cm_repo/pkg/src/a" "$cm_repo/pkg/src/bin"
|
||
printf '[package]\nname = "p"\n' >"$cm_repo/pkg/Cargo.toml"; : >"$cm_repo/pkg/src/lib.rs"; $c add -A 2>/dev/null; $c commit -qm c1
|
||
for f in src/a/mod.rs src/a/b.rs; do echo "// $f" >"$cm_repo/pkg/$f"; done
|
||
[ "$1" = C ] || for f in src/lib.rs src/main.rs src/bin/x.rs build.rs; do echo "// $f" >"$cm_repo/pkg/$f"; done
|
||
fi
|
||
$c add -A 2>/dev/null; $c commit -qm c2
|
||
}
|
||
cmtable() { # cmtable A|B|C: sets cm_table (name <TAB> outcome under a filtered run <TAB> outcome under the full suite)
|
||
cm_table=$(mktemp)
|
||
if [ "$1" = A ]; then
|
||
local p=crates/x/src
|
||
printf '%s\n' \
|
||
"$p/alpha/mod.rs:2:5: replace add -> i32 with 0 caught caught" \
|
||
"$p/alpha/mod.rs:7:7: replace * with + in double missed caught" \
|
||
"$p/alpha/mod.rs:9:9: replace && with || in check missed missed" \
|
||
"$p/alpha/mod.rs:12:5: replace f(x) -> Vec<i32> with vec![1] caught caught" \
|
||
"$p/alpha/mod.rs:15:5: replace id -> Option<i32> with None unviable unviable" \
|
||
"$p/alpha/inner.rs:3:5: replace slow -> u32 with 0 timeout caught" \
|
||
"$p/alpha/inner.rs:6:5: replace spin -> u32 with 1 missed timeout" \
|
||
"$p/beta.rs:2:5: replace b1 -> i32 with 0 missed caught" \
|
||
"$p/beta.rs:3:5: replace b2 -> i32 with 0 missed missed" \
|
||
"$p/beta.rs:4:5: replace b3 -> i32 with 0 missed unviable" \
|
||
"$p/gamma.rs:2:5: replace g -> i32 with 0 caught caught" \
|
||
"$p/lib.rs:2:5: replace l1 -> i32 with 0 missed caught" \
|
||
"$p/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1] missed missed" \
|
||
"$p/main.rs:2:5: replace m -> i32 with 0 missed caught" \
|
||
"crates/x/build.rs:2:5: replace bu -> i32 with 0 missed caught" \
|
||
"$p/bin/tool.rs:2:5: replace t -> i32 with 0 missed caught" >"$cm_table"
|
||
elif [ "$1" = C ]; then
|
||
printf '%s\n' "src/a/mod.rs:2:5: replace a -> i32 with 0 caught caught" "src/a/b.rs:2:5: replace a -> i32 with 0 caught caught" >"$cm_table"
|
||
else
|
||
printf '%s\n' "src/lib.rs:2:5: replace a -> i32 with 0 caught caught" "src/main.rs:2:5: replace a -> i32 with 0 caught caught" \
|
||
"src/bin/x.rs:2:5: replace a -> i32 with 0 caught caught" "build.rs:2:5: replace a -> i32 with 0 caught caught" \
|
||
"src/a/mod.rs:2:5: replace a -> i32 with 0 caught caught" "src/a/b.rs:2:5: replace a -> i32 with 0 caught caught" >"$cm_table"
|
||
fi
|
||
}
|
||
# cm <A|B> [VAR=value]...: runs the launcher; sets cm_out, cm_rc, cm_root, cm_fd, cm_log. HOLD_OWN=1 holds the job's own slot as
|
||
# ygg-verify does (a descriptor the driver inherits); HOLD_OTHERS=<n> holds n further slots as other jobs would (separate processes); PSI=<n> is the io pressure file's full avg60.
|
||
cm() {
|
||
local lay=$1; shift
|
||
cmrepo "$lay"; cmtable "$lay"
|
||
cm_root=$(dirname "$cm_repo"); cm_fd=$cm_root/fd; cm_log=$cm_root/cargo.log
|
||
mkdir -p "$cm_fd" "$cm_root/tools/stopgap" "$cm_root/ygg" "$cm_root/tmp"
|
||
cp "$here/cargo-mutants.sh" "$cm_root/tools/"
|
||
printf '#!/usr/bin/env bash\n# test stub of ygg-verify.sh RUN_ON_HOST=1: <repo> <rev> <image> <cmd>; runs cmd as the host half inside the revision\ncd "$YGG_WORK" && exec bash -o pipefail -c "$4"\n' >"$cm_root/tools/stopgap/ygg-verify.sh"
|
||
local holders=() i n own=0 others=0 psi=0.00 kv env=()
|
||
for kv in "$@"; do case $kv in HOLD_OWN=*) own=${kv#*=} ;; HOLD_OTHERS=*) others=${kv#*=} ;; PSI=*) psi=${kv#*=} ;; *) env+=("$kv") ;; esac; done
|
||
for i in $(seq 1 "$others"); do
|
||
perl -e 'use Fcntl ":flock"; open(F, ">>", $ARGV[0]) or die; flock(F, LOCK_EX) or die; open(R, ">", "$ARGV[0].ready"); close R; sleep 120' "$cm_root/ygg/slot-$((i + 1)).lock" & holders+=($!)
|
||
for n in $(seq 1 50); do [ -e "$cm_root/ygg/slot-$((i + 1)).lock.ready" ] && break; sleep 0.1; done
|
||
done
|
||
cm_out=$( ( if [ "$own" = 1 ]; then exec {ownfd}>"$cm_root/ygg/slot-1.lock"; PATH=$CMB:$PATH flock -n "$ownfd"; fi
|
||
export PATH=$CMB:$PATH FAKE_DOCKER=$cm_fd FAKE_CM_TABLE=$cm_table FAKE_CM_LOG=$cm_log FAKE_CM_TESTS=$cm_root/tests.tsv \
|
||
FAKE_WSROOT=$([ "$lay" = A ] || echo pkg/) YGG_WORK=$cm_repo YGG_IMAGE=rust YGG_CNAME=job1 YGG_LABEL=eureka-verify-job=job1 YGG_CPUS=1 YGG_MEM=1g \
|
||
YGG_SLOTS=5 YGG_ROOT=$cm_root/ygg YGG_NONCE=n0nce1 CM_PSI=$cm_root/psi TMPDIR=$cm_root/tmp
|
||
printf 'beta::\t0\n' >"$cm_root/tests.tsv"
|
||
printf 'some avg10=0.00 avg60=0.00 avg300=0.00 total=0\nfull avg10=0.00 avg60=%s avg300=0.00 total=0\n' "$psi" >"$cm_root/psi"
|
||
for kv in "${env[@]}"; do export "${kv?}"; done
|
||
timeout 240 bash "$cm_root/tools/cargo-mutants.sh" "$cm_repo" HEAD HEAD~1 "$cm_wd" 2>&1 ) )
|
||
cm_rc=$?
|
||
for i in "${holders[@]}"; do kill "$i" 2>/dev/null; done; wait 2>/dev/null
|
||
}
|
||
cmcheck() { # cmcheck <name> <must-contain | !must-not-contain>...
|
||
local name=$1 s; shift
|
||
for s in "$@"; do
|
||
case $s in
|
||
'!'*) if printf '%s\n' "$cm_out" | grep -qF -- "${s#!}"; then echo "FAIL cargo-mutants $name: must not contain: ${s#!}"; printf '%s\n' "$cm_out" | tail -n 12; fail=1; return; fi ;;
|
||
*) printf '%s\n' "$cm_out" | grep -qF -- "$s" || { echo "FAIL cargo-mutants $name: missing: $s"; printf '%s\n' "$cm_out" | tail -n 14; fail=1; return; } ;;
|
||
esac
|
||
done
|
||
echo "ok cargo-mutants $name"
|
||
}
|
||
cmrc() { [ "$cm_rc" = "$2" ] || { echo "FAIL cargo-mutants $1: exit $cm_rc, want $2"; printf '%s\n' "$cm_out" | tail -n 8; fail=1; }; }
|
||
cmclean() { # nothing the job made may outlive it: no container directory, no bundle, no staging directory
|
||
local left; left=$(ls "$cm_fd/c" "$cm_root/tmp" 2>/dev/null | grep -v '^$' | grep -v ':$' | paste -sd, -)
|
||
if [ -z "$left" ]; then echo "ok cargo-mutants $1: containers, bundle and staging removed"; else echo "FAIL cargo-mutants $1: left behind: $left"; fail=1; fi
|
||
}
|
||
cmlocks() { # cmlocks <case>: after the driver exits, every slot lock can be taken again by a fresh process
|
||
local i bad=''
|
||
for i in 1 2 3 4 5; do bash -c "exec {fd}>'$cm_root/ygg/slot-$i.lock'; PATH=$CMB:\$PATH flock -n \$fd" || bad="$bad $i"; done
|
||
if [ -z "$bad" ]; then echo "ok cargo-mutants $1: every slot lock is free again"; else echo "FAIL cargo-mutants $1: slots still locked:$bad"; fail=1; fi
|
||
}
|
||
|
||
# filter derivation, stage-1 survivors and timeouts confirmed, zero-test filter, no raw stage-1 lines, exit 2 and 3 normal
|
||
cm A TEST_ARGS='--skip flaky'
|
||
cmrc A 0
|
||
cmcheck A-filters \
|
||
'FILTER crates/x/src/alpha/mod.rs alpha:: tests=1 mutants=5' 'FILTER crates/x/src/alpha/inner.rs alpha::inner:: tests=1 mutants=2' \
|
||
'FILTER crates/x/src/gamma.rs gamma:: tests=1 mutants=1' \
|
||
'!FILTER crates/x/src/lib.rs' '!FILTER crates/x/src/main.rs' '!FILTER crates/x/src/bin/tool.rs' '!FILTER crates/x/build.rs'
|
||
cmcheck A-zero-test-filter 'FILTER crates/x/src/beta.rs beta:: tests=0 mutants=3' 'caught=10 missed=3 timeout=1 unviable=2'
|
||
cmcheck A-confirmed-and-counted 'MUTANTS tested=16 caught=10 missed=3 timeout=1 unviable=2 | stage1 filtered=8 survivors=4 confirmed_caught=2 | containers=3 |' \
|
||
'!replace * with + in double' '!replace slow -> u32 with 0' '!replace add -> i32' \
|
||
'MISSED crates/x/src/alpha/mod.rs:9:9: replace && with || in check' 'MISSED crates/x/src/beta.rs:3:5: replace b2' 'MISSED crates/x/src/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1]' \
|
||
'TIMEOUT crates/x/src/alpha/inner.rs:6:5: replace spin'
|
||
if [ "$(printf '%s\n' "$cm_out" | grep -c "^YGG-SUMMARY/n0nce1: MISSED ")" = 3 ]; then echo "ok cargo-mutants A-missed-once: 3 final MISSED lines"; else echo "FAIL cargo-mutants A-missed-once"; fail=1; fi
|
||
if printf '%s\n' "$cm_out" | grep -qE '^(MISSED|TIMEOUT) '; then echo "FAIL cargo-mutants A-no-raw-lines: a MISSED or TIMEOUT line outside the nonce-tagged lines"; fail=1; else echo "ok cargo-mutants A-no-raw-lines"; fi
|
||
if grep -qF 'stage=1 filter=alpha:: harness=--skip flaky' "$cm_log" && grep -qF 'stage=1 filter=alpha::inner:: harness=--skip flaky' "$cm_log" && grep -qF 'stage=2 filter= harness=--skip flaky' "$cm_log"
|
||
then echo "ok cargo-mutants A-harness-args: TEST_ARGS follow the filter in stage 1 and stand alone in stage 2"; else echo "FAIL cargo-mutants A-harness-args"; sort -u "$cm_log"; fail=1; fi
|
||
if [ "$(grep -c '^stage=2' "$cm_log")" = 3 ]; then echo "ok cargo-mutants A-stage2-sharded: 3 full-suite shards"; else echo "FAIL cargo-mutants A-stage2-sharded"; fail=1; fi
|
||
# a file whose filter selects 0 tests is found at plan time and never pays a stage-1 pass; its names run in stage 2
|
||
if grep -qF 'count filter=beta:: harness=--skip flaky' "$cm_log" && ! grep -q '^stage=1 filter=beta::' "$cm_log"
|
||
then echo "ok cargo-mutants A-zero-test-plan: counted at plan time, no stage-1 run for it"; else echo "FAIL cargo-mutants A-zero-test-plan"; sort -u "$cm_log"; fail=1; fi
|
||
# the stage-2 timeout bound is the generous default with a floor, not a tight multiple of a quiet-host baseline
|
||
if [ "$(grep -c '^stage=2 .* mult=5 min=20$' "$cm_log")" = 3 ] && ! grep '^stage=2' "$cm_log" | grep -qv 'mult=5 min=20$'
|
||
then echo "ok cargo-mutants A-timeout-bound: stage 2 runs at 5x with a 20 s floor"; else echo "FAIL cargo-mutants A-timeout-bound"; grep '^stage=2' "$cm_log"; fail=1; fi
|
||
cmclean A; cmlocks A
|
||
|
||
# derivation on a standalone package under the repo root: the diff is taken at the workspace root (--relative)
|
||
cm B
|
||
cmrc B 0
|
||
cmcheck B-derivation 'FILTER src/a/mod.rs a:: tests=1 mutants=1' 'FILTER src/a/b.rs a::b:: tests=1 mutants=1' \
|
||
'!FILTER src/lib.rs' '!FILTER src/main.rs' '!FILTER src/bin/x.rs' '!FILTER build.rs' 'MUTANTS tested=6 caught=6 missed=0'
|
||
# FILTERS overrides the derived filter; '-' forces the full suite
|
||
cm B 'FILTERS=src/a/b.rs=-,src/lib.rs=special::'
|
||
cmcheck B-override 'FILTER src/lib.rs special:: tests=1 mutants=1' 'FILTER src/a/mod.rs a:: ' '!FILTER src/a/b.rs' 'tested=6 caught=6'
|
||
|
||
# stage 1 settles every mutant: the full-suite baseline still runs (stage 2 is never skipped), and a red one is a red job
|
||
cm C
|
||
cmrc C-settled 0
|
||
cmcheck C-settled 'MUTANTS tested=2 caught=2 missed=0 timeout=0 unviable=0 | stage1 filtered=2 survivors=0 confirmed_caught=0'
|
||
if [ "$(grep -c '^stage=2' "$cm_log")" = 1 ]; then echo "ok cargo-mutants C-baseline-runs: one full-suite run although stage 1 settled every mutant"; else echo "FAIL cargo-mutants C-baseline-runs"; sort -u "$cm_log"; fail=1; fi
|
||
cm C FAKE_CM_RED=2
|
||
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants C-red-baseline: exit 0"; fail=1; }
|
||
cmcheck C-red-baseline 'ERROR stage 2 (shard 1 of 1)' 'container exited 4' '!MUTANTS'
|
||
cmclean C-red-baseline
|
||
# CONFIRM_TIMEOUT_MULT and MIN_TEST_TIMEOUT reach the stage-2 run
|
||
cm A CONFIRM_TIMEOUT_MULT=9 MIN_TEST_TIMEOUT=33
|
||
if grep '^stage=2' "$cm_log" | grep -q 'mult=9 min=33$' && ! grep '^stage=2' "$cm_log" | grep -qv 'mult=9 min=33$'; then echo "ok cargo-mutants timeout-overrides: 9x with a 33 s floor in stage 2"; else echo "FAIL cargo-mutants timeout-overrides"; grep '^stage=2' "$cm_log"; fail=1; fi
|
||
# slot lease: the job's own slot is held by ygg-verify (an inherited descriptor), others by other jobs
|
||
cm A HOLD_OWN=1 HOLD_OTHERS=1; cmcheck lease-free 'containers=3 |'; cmlocks lease-free
|
||
cm A HOLD_OWN=1 HOLD_OTHERS=3; cmcheck lease-one-free 'containers=2 |'
|
||
cm A HOLD_OWN=1 HOLD_OTHERS=4; cmcheck lease-none-free 'containers=1 |' 'tested=16'; cmlocks lease-none-free
|
||
open_none=$(grep -o 'openfds=[0-9]*' "$cm_log" | sort -u | paste -sd, -)
|
||
cm A HOLD_OWN=1 HOLD_OTHERS=1; open_leased=$(grep -o 'openfds=[0-9]*' "$cm_log" | sort -u | paste -sd, -)
|
||
if [ "$open_none" = "$open_leased" ]; then echo "ok cargo-mutants lease-not-inherited: containers see the same descriptors with 2 leases held as with none ($open_none)"; else echo "FAIL cargo-mutants lease-not-inherited: $open_none vs $open_leased"; fail=1; fi
|
||
cm A PSI=41.00; cmcheck lease-io-pressure 'containers=1 |' 'tested=16'
|
||
# a red baseline, in stage 1 or in stage 2, is not a clean run
|
||
cm A FAKE_CM_RED=1
|
||
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants red-stage1: exit 0"; fail=1; }
|
||
cmcheck red-stage1 'ERROR stage 1 (' 'container exited 4' '!MUTANTS' '!MISSED' '!TIMEOUT'
|
||
cm A FAKE_CM_RED=2
|
||
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants red-stage2: exit 0"; fail=1; }
|
||
cmcheck red-stage2 'ERROR stage 2 (shard' 'container exited 4' '!MUTANTS' '!MISSED' '!TIMEOUT'
|
||
cmclean red-stage2
|
||
# NAMES rerun: no stage-1 container, only the listed names, on the full suite
|
||
cm_names=$(mktemp)
|
||
printf '%s\n' 'crates/x/src/alpha/mod.rs:9:9: replace && with || in check' 'crates/x/src/lib.rs:3:5: replace f(x) -> Vec<i32> with vec![1]' 'crates/x/src/beta.rs:2:5: replace b1 -> i32 with 0' >"$cm_names"
|
||
cm A "NAMES=$cm_names"
|
||
cmrc names 0
|
||
cmcheck names 'MUTANTS tested=3 caught=1 missed=2 timeout=0 unviable=0 | stage1 filtered=0 survivors=0 confirmed_caught=0' 'MISSED crates/x/src/alpha/mod.rs:9:9' '!FILTER'
|
||
if grep -q 'run job1-s1' "$cm_fd/log" || grep -q '^stage=1' "$cm_log"; then echo "FAIL cargo-mutants names: a stage-1 container or a filtered run"; fail=1; else echo "ok cargo-mutants names: no stage-1 container"; fi
|
||
# a name the range does not produce is an error that does not echo the name
|
||
printf '%s\n' 'crates/x/src/lib.rs:99:9: replace no_such_secret_name -> i32 with 0' >"$cm_names"
|
||
cm A "NAMES=$cm_names"
|
||
[ "$cm_rc" != 0 ] || { echo "FAIL cargo-mutants names-unknown: exit 0"; fail=1; }
|
||
cmcheck names-unknown 'ERROR 1 mutants selected' '!no_such_secret_name' '!MUTANTS'
|
||
rm -f "$cm_names"
|
||
# killed: a job the watchdog already killed starts nothing and leaves nothing
|
||
cm_k=$(mktemp); cm A "YGG_KILLED=$cm_k"; rm -f "$cm_k"
|
||
cmrc killed-before 124
|
||
if grep -q '^run ' "$cm_fd/log" 2>/dev/null; then echo "FAIL cargo-mutants killed-before: a container started"; fail=1; else echo "ok cargo-mutants killed-before: no container started"; fi
|
||
cmclean killed-before
|
||
# the flag appears while stage 1 runs: stage 2 never starts
|
||
cm_k=$(mktemp -u); cm A "YGG_KILLED=$cm_k" "FAKE_CM_KILLS=$cm_k"; rm -f "$cm_k"
|
||
cmrc killed-during 124
|
||
cmcheck killed-during 'JOB KILLED' '!MUTANTS'
|
||
if grep -q 'run job1-s2' "$cm_fd/log"; then echo "FAIL cargo-mutants killed-during: a stage-2 container started"; fail=1; else echo "ok cargo-mutants killed-during: no stage-2 container"; fi
|
||
cmclean killed-during
|
||
# the launcher refuses bad input before any job starts, and never echoes the value it refused
|
||
cmrepo A
|
||
for v in 'SHARDS=0' 'SHARDS=x;SECRET' 'FILTERS=nopairSECRET' 'CONFIRM_TIMEOUT_MULT=2;SECRET'; do
|
||
lo=$(env "$v" bash "$here/cargo-mutants.sh" "$cm_repo" HEAD HEAD~1 "$cm_wd" 2>&1); lrc=$?
|
||
if [ "$lrc" = 2 ] && ! printf '%s' "$lo" | grep -q SECRET; then echo "ok cargo-mutants launcher refuses ${v%%=*}"; else echo "FAIL cargo-mutants launcher ${v%%=*}: exit $lrc, output: $lo"; fail=1; fi
|
||
done
|
||
lo=$(bash "$here/cargo-mutants.sh" "$cm_repo" HEAD~1 HEAD "$cm_wd" 2>&1); lrc=$?
|
||
if [ "$lrc" = 2 ] && printf '%s' "$lo" | grep -q 'not an ancestor'; then echo "ok cargo-mutants launcher refuses a base that is not an ancestor"; else echo "FAIL cargo-mutants launcher base: exit $lrc, output: $lo"; fail=1; fi
|
||
|
||
# the -F escaper: every regex metacharacter, the pipe included, is escaped; nothing else is touched
|
||
esc_bad=''
|
||
for c in '\' . + '*' '?' '(' ')' '|' '[' ']' '{' '}' '^' '$'; do
|
||
got=$(printf 'a%sb\n' "$c" | bash "$here/cargo-mutants.sh" --escape)
|
||
[ "$got" = '^a'\\"$c"'b$' ] || esc_bad="$esc_bad [$c]"
|
||
done
|
||
got=$(printf 'replace x -> Vec<i32> with vec![1] & 2 - 3 #\n' | bash "$here/cargo-mutants.sh" --escape)
|
||
[ "$got" = '^replace x -> Vec<i32> with vec!\[1\] & 2 - 3 #$' ] || esc_bad="$esc_bad [plain]"
|
||
if [ -z "$esc_bad" ]; then echo "ok cargo-mutants escaper: all 14 metacharacters escaped, others literal"; else echo "FAIL cargo-mutants escaper:$esc_bad"; fail=1; fi
|
||
|
||
# wait-on.sh: pid, log and file forms, and the vanished-process exit
|
||
W=$here/wait-on.sh
|
||
wo() { local rc; WAIT_MAX=${WM:-4} bash "$W" "$@" >"$T/wo.out" 2>&1; rc=$?; echo "$rc"; }
|
||
wchk() { if [ "$3" = "$2" ]; then echo "ok wait-on $1: exit $3"; else echo "FAIL wait-on $1: exit $3, want $2"; fail=1; fi; }
|
||
sleep 3 & sp=$!; printf 'one\ntwo\n' >"$T/w.log"
|
||
wchk "pid finishes" 0 "$(WM=10 wo pid $sp "$T/w.log")"
|
||
sleep 20 & sp=$!
|
||
r=$(WM=2 wo pid $sp); wchk "pid still running" 99 "$r"
|
||
grep -q '^RUNNING ' "$T/wo.out" || { echo "FAIL wait-on: no RUNNING line"; fail=1; }
|
||
kill $sp 2>/dev/null; wait $sp 2>/dev/null
|
||
r=$(WM=10 wo pid $sp "$T/w.log"); wchk "killed pid is done" 0 "$r"
|
||
grep -q two "$T/wo.out" || { echo "FAIL wait-on pid: no log tail"; fail=1; }
|
||
: >"$T/l.log"; ( sleep 2; echo "step 1" >>"$T/l.log"; echo "all done ok" >>"$T/l.log" ) &
|
||
r=$(WM=10 wo log "$T/l.log" 'done ok'); wchk "log gains its done line" 0 "$r"
|
||
grep -q 'all done ok' "$T/wo.out" || { echo "FAIL wait-on log: no tail"; fail=1; }
|
||
r=$(WM=2 wo log "$T/l.log" 'never-appears'); wchk "log never matches" 99 "$r"
|
||
printf 'partial\n' >"$T/v.log"; sleep 1 & vp=$!; wait $vp
|
||
r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, no completion" 98 "$r"
|
||
printf 'FINISHED\n' >>"$T/v.log"
|
||
r=$(WAIT_PID=$vp WM=10 wo log "$T/v.log" 'FINISHED'); wchk "vanished pid, completion logged" 0 "$r"
|
||
r=$(WM=2 wo log "$T/none.log" 'x'); wchk "log missing, no pid, short cap" 99 "$r"
|
||
( sleep 2; echo ready >"$T/f.txt" ) &
|
||
r=$(WM=10 wo file "$T/f.txt"); wchk "file appears" 0 "$r"
|
||
: >"$T/e.txt"; r=$(WM=2 wo file "$T/e.txt"); wchk "file empty is not done" 99 "$r"
|
||
# F2: the pid form reports GONE, never anything that reads as success
|
||
sleep 1 & gp=$!; wait $gp
|
||
r=$(WM=4 wo pid $gp); wchk "pid gone" 0 "$r"
|
||
grep -q '^GONE (no completion evidence)' "$T/wo.out" || { echo "FAIL wait-on pid: no GONE line"; fail=1; }
|
||
# F4: an invalid ERE is a usage error, not a forever RUNNING
|
||
r=$(WM=2 wo log "$T/l.log" '(unclosed'); wchk "invalid regex" 3 "$r"
|
||
grep -q 'not a valid extended regex' "$T/wo.out" || { echo "FAIL wait-on: no regex message"; fail=1; }
|
||
r=$(wo bogus); wchk "bad usage" 2 "$r"
|
||
wait
|
||
|
||
# context-pack.py: a spec's anchors carried from its base to the worktree (no network)
|
||
CP=$here/context-pack.py; CF=$here/fixtures/context-pack
|
||
if py -3 -c pass >/dev/null 2>&1; then PY="py -3"; elif python3 -c pass >/dev/null 2>&1; then PY=python3; else PY=python; fi
|
||
wrapcp() { $PY -c 'import sys,json; v=json.load(sys.stdin); v=v if "kind" in v else {"kind":"cut_spec","value":dict(campaign="t",cut="x",revision=1,**v)}; json.dump(v,sys.stdout)'; }
|
||
wp() { cygpath -m "$1" 2>/dev/null || echo "$1"; }
|
||
R=$T/cp; mkdir -p "$R"; Rw=$(wp "$R")
|
||
g() { git -C "$R" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
|
||
seqf() { local i; for i in $(seq 1 "$2"); do echo "$1$i"; done; }
|
||
g init -q
|
||
seqf a 40 >"$R/a.txt"; seqf b 40 >"$R/b.txt"; seqf c 10 >"$R/c.txt"; seqf d 20 >"$R/d.txt"
|
||
seqf e 10 >"$R/e.txt"; seqf f 20 >"$R/f.txt"; seqf g 300 >"$R/big.txt"; seqf h 5 | sed 's/$/
|
||
/' >"$R/h.txt"; seqf m 40 >"$R/m.txt"
|
||
printf 's1\ns2\ns3\ns4\ndef needle_three\ns6\ns7 needle_many\ns8\nfoo::bar\ns10\n' >"$R/sym.txt"
|
||
printf 'needle_three call 1\nneedle_three call 2\nneedle_three call 3\nneedle_threefold is another word\n' >"$R/zcallers.txt"
|
||
echo 'bound_fifty bound_fiftyone spread_sym long_symbol q.zq sym_bad twenty_sym' >"$R/anchors.txt"
|
||
seqf "bound_fifty x" 50 >"$R/bound50.txt"; seqf "bound_fiftyone x" 51 >"$R/bound51.txt"; seqf "q.zq x" 51 >"$R/dotted.txt"
|
||
printf 'long_symbol %s%s\r' "$(printf '%0150d' 0 | tr 0 z)" "$(printf '%0100d' 0 | tr 0 y)" >"$R/longline.txt"; printf '\n' >>"$R/longline.txt"
|
||
printf 'sym_bad \377 endX\r\n' >"$R/bad.txt"
|
||
mkdir -p "$R/manyf"; for i in $(seq -w 1 21); do seqf "spread_sym $i-" 3 >"$R/manyf/f$i.txt"; done; echo "spread_sym extra" >>"$R/manyf/f21.txt"
|
||
mkdir -p "$R/twf"; for i in $(seq -w 1 20); do seqf "twenty_sym $i-" 3 >"$R/twf/f$i.txt"; done
|
||
seqf "needle_many x." 30 >"$R/many1.txt"; seqf "needle_many y." 30 >"$R/many2.txt"
|
||
g add -A; g commit -q -m base; BASE=$(git -C "$R" rev-parse HEAD)
|
||
{ sed -n 1,5p "$R/a.txt"; printf 'ins1\nins2\nins3\n'; sed -n '6,$p' "$R/a.txt"; } >"$R/a.new"; mv "$R/a.new" "$R/a.txt"
|
||
sed -e '2,3d' -e 's/^b10$/b10-EDITED/' "$R/b.txt" >"$R/b.new"; mv "$R/b.new" "$R/b.txt"
|
||
sed -e '10,14c\X1\nX2' -e '30,31d' "$R/m.txt" >"$R/m.new"; mv "$R/m.new" "$R/m.txt"
|
||
g rm -q c.txt; g mv d.txt d2.txt; g add -A; g commit -q -m second
|
||
sed -i 's/^f5$/f5-UNCOMMITTED/' "$R/f.txt"; printf 'n1\nn2\nn3\n' >"$R/n.txt"
|
||
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec.json" | wrapcp >"$T/cp-spec.json"; SP=$(wp "$T/cp-spec.json")
|
||
cpo() { local s=$1; shift; $PY "$CP" "$s" "$Rw" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
|
||
cchk() { if [ "$3" = "$2" ]; then echo "ok context-pack $1: exit $3"; else echo "FAIL context-pack $1: exit $3, want $2"; fail=1; fi; }
|
||
cin() { grep -qF -- "$2" "$T/cp.out" && echo "ok context-pack $1" || { echo "FAIL context-pack $1: missing: $2"; fail=1; }; }
|
||
cnot() { if grep -qF -- "$2" "$T/cp.out" "$T/cp.err"; then echo "FAIL context-pack $1: must not contain: $2"; fail=1; else echo "ok context-pack $1"; fi; }
|
||
r=$(cpo "$SP"); cchk "fixture (GONE and OUT_OF_RANGE)" 3 "$r"
|
||
cin "header" "context-pack t cut-x.r1 base ${BASE:0:12} HEAD"
|
||
cin "header counts modified files" " modified 6"
|
||
cin "MOVED(+3)" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
|
||
cin "insertion at the last line is outside" "[2] a.txt:3-5 SAME -> a.txt:3-5"
|
||
cin "insertion before the range shifts it" "[3] a.txt:6-7 MOVED(+3) -> a.txt:9-10"
|
||
cin "insertion inside widens" "[4] a.txt:4-8 TOUCHED(+3/-0 inside) -> a.txt:4-11"
|
||
cin "delete above, edit inside" "[5] b.txt:8-12 TOUCHED(+1/-1 inside) -> b.txt:6-10"
|
||
cin "MOVED(-2)" "[6] b.txt:30-31 MOVED(-2) -> b.txt:28-29"
|
||
cin "GONE" "[7] c.txt:2-3 GONE (not named by the spec's deletes)"
|
||
cin "RENAMED" "[8] d.txt:4-5 RENAMED -> d2.txt SAME -> d2.txt:4-5"
|
||
cin "NEW with basename candidates" "[9] sub/a.txt:1-2 NEW (at base: a.txt)"
|
||
cin "OUT_OF_RANGE" "[10] e.txt:5-50 OUT_OF_RANGE (base file has 10 lines)"
|
||
cin "uncommitted edit is TOUCHED" "[11] f.txt:5-6 TOUCHED(+1/-1 inside)"
|
||
cin "last line of the file is in range" "[12] e.txt:10-10 SAME"
|
||
cin "one past the last line is out of range" "[13] e.txt:10-11 OUT_OF_RANGE (base file has 10 lines)"
|
||
cin "CRLF file reads SAME" "[14] h.txt:2-3 SAME"
|
||
cin "hunk straddling the head clamps to the new block" "[15] m.txt:12-13 TOUCHED(+2/-5 inside) -> m.txt:10-11"
|
||
cin "hunk starting inside and ending past the range" "[16] m.txt:8-11 TOUCHED(+2/-5 inside) -> m.txt:8-11"
|
||
cin "range just after a replaced block shifts" "[17] m.txt:15-16 MOVED(-3) -> m.txt:12-13"
|
||
cin "deletion after the range moves nothing else" "[18] m.txt:28-28 MOVED(-3) -> m.txt:25-25"
|
||
cin "deletion at the next line" "[19] m.txt:29-29 MOVED(-3) -> m.txt:26-26"
|
||
cin "deletion ending just before the range" "[20] m.txt:32-32 MOVED(-5) -> m.txt:27-27"
|
||
cin "range after both hunks" "[21] m.txt:33-33 MOVED(-5) -> m.txt:28-28"
|
||
cin "insertion at the first line is inside" "[22] a.txt:5-7 TOUCHED(+3/-0 inside) -> a.txt:5-10"
|
||
cin "hunk ending just before the range shifts it" "[23] b.txt:4-5 MOVED(-2) -> b.txt:2-3"
|
||
cin "NEW file present in the worktree gets a window" "== [24] n.txt:1-2"
|
||
cin "NEW window shows the file" " 2 n2"
|
||
cin "CRLF window text" " 2 h2"
|
||
{ tr -d '
|
||
' <"$T/cp.out" | cmp -s - "$T/cp.out"; } && echo "ok context-pack CRLF is stripped from windows" || { echo "FAIL context-pack CR in output"; fail=1; }
|
||
cin "window numbers are worktree lines" " 6 b8"
|
||
cin "window shows the edit" "b10-EDITED"
|
||
cin "GONE window is labelled AT BASE" "== [7] c.txt:2-3 AT BASE"
|
||
cin "GONE window shows base text" " 2 c2"
|
||
cin "renamed window reads the new path" "== [8] d2.txt:4-5"
|
||
[ "$(grep -c '^\[' "$T/cp.out")" = 24 ] && echo "ok context-pack table lists every anchor" || { echo "FAIL context-pack table count"; fail=1; }
|
||
{ sed -n 2p "$T/cp.out" | grep -q '^\[7\]' && sed -n 6p "$T/cp.out" | grep -q '^\[24\]' && sed -n 7p "$T/cp.out" | grep -q '^\[1\]'; } && echo "ok context-pack problem anchors first" || { echo "FAIL context-pack problem anchors first"; fail=1; }
|
||
# deletes naming the file, and a valid range, make the same spec exit 0
|
||
sed -e 's/"deletes": \[\]/"deletes": [{"path": "c.txt"}]/' -e 's/"end_line": 50/"end_line": 6/' -e 's/"end_line": 11/"end_line": 10/' "$T/cp-spec.json" | wrapcp >"$T/cp-spec0.json"
|
||
r=$(cpo "$(wp "$T/cp-spec0.json")"); cchk "GONE named by deletes, no OUT_OF_RANGE" 0 "$r"
|
||
cin "GONE in deletes has no qualifier" "[7] c.txt:2-3 GONE #"
|
||
# unreachable base
|
||
sed "s/\"$BASE\"/\"deadbeef\"/" "$T/cp-spec.json" | wrapcp >"$T/cp-specb.json"
|
||
r=$(cpo "$(wp "$T/cp-specb.json")"); cchk "unreachable base" 3 "$r"
|
||
cin "UNRESOLVED hint" "UNRESOLVED base not a commit here: git fetch origin deadbeef"
|
||
[ "$(grep -c 'UNRESOLVED' "$T/cp.out")" = 24 ] && echo "ok context-pack every anchor UNRESOLVED" || { echo "FAIL context-pack UNRESOLVED count"; fail=1; }
|
||
cnot "UNRESOLVED prints no window" "== ["
|
||
# --since: every anchor stays in the table; only the edited one has a window
|
||
r=$(cpo "$SP" --since HEAD); cchk "--since" 3 "$r"
|
||
cin "--since keeps the table" "[1] a.txt:20-22 MOVED(+3)"
|
||
cin "--since prints the edited window" "== [11] f.txt:5-6"
|
||
cin "--since window shows the edit" "f5-UNCOMMITTED"
|
||
cnot "--since omits untouched windows" "== [1] "
|
||
cnot "--since omits the GONE window" "== [7] "
|
||
r=$(cpo "$SP" --since "$BASE"); cin "--since an older commit widens" "== [4] a.txt:4-11"
|
||
cnot "--since an older commit still omits untouched windows" "== [2] "
|
||
r=$(cpo "$SP" --since "$BASE" --only 15,16,17,18,19,20,21)
|
||
for k in 15 16 19 20; do cin "--since window $k overlaps a hunk" "== [$k] "; done
|
||
for k in 17 18 21; do cnot "--since window $k does not overlap" "== [$k] "; done
|
||
r=$(cpo "$SP" --only 2,11); cin "--only prints the named window" "== [2] "
|
||
cnot "--only omits the others" "== [1] "
|
||
r=$(cpo "$SP" --only 99); cchk "--only outside file_changes" 2 "$r"; grep -qF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack names --only" || { echo "FAIL context-pack names --only"; fail=1; }
|
||
r=$(cpo "$SP" --since feedface); cchk "--since not a commit" 2 "$r"; grep -qF "context-pack: --since: not a commit in this worktree" "$T/cp.err" && echo "ok context-pack names --since" || { echo "FAIL context-pack names --since"; fail=1; }
|
||
Z70=$(printf 'z%.0s' $(seq 1 70))
|
||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
|
||
{"change": "up", "location": {"path": "../a.txt", "line": 1, "end_line": 1}},
|
||
{"change": "abs", "location": {"path": "/a.txt", "line": 1, "end_line": 1}},
|
||
{"change": "drive", "location": {"path": "c:a.txt", "line": 1, "end_line": 1}},
|
||
{"change": "'$Z70'zzzz", "location": {"path": "a.txt", "line": 1, "end_line": 1}}]}' | wrapcp >"$T/cp-specu.json"
|
||
r=$(cpo "$(wp "$T/cp-specu.json")"); cchk "unsafe paths" 3 "$r"
|
||
[ "$(grep -c 'UNRESOLVED path or git diff unusable' "$T/cp.out")" = 3 ] && echo "ok context-pack unsafe paths are UNRESOLVED" || { echo "FAIL context-pack unsafe paths"; fail=1; }
|
||
cin "a long change is cut at 70 characters" "# $Z70"
|
||
cnot "a long change is cut at 70 characters (not 71)" "${Z70}z"
|
||
# caps
|
||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
|
||
for i in $(seq 1 30); do echo '{"change": "c'$i'", "location": {"path": "big.txt", "line": '$((i*5))', "end_line": '$((i*5+9))'}}'$([ $i -lt 30 ] && echo ,); done
|
||
echo ']}'; } | wrapcp >"$T/cp-spec30.json"
|
||
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 200); cchk "30 anchors" 0 "$r"
|
||
[ "$(wc -l <"$T/cp.out")" -le 200 ] && echo "ok context-pack output within --max-lines" || { echo "FAIL context-pack over 200 lines"; fail=1; }
|
||
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack capped output keeps the full table" || { echo "FAIL context-pack capped table"; fail=1; }
|
||
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && echo "ok context-pack expands 9 windows of 17 lines" || { echo "FAIL context-pack window count: $(grep -c '^== \[' "$T/cp.out")"; fail=1; }
|
||
cin "rest listed as not expanded" "not expanded: --only 10,11,12"
|
||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}}]}' | wrapcp >"$T/cp-spec1.json"
|
||
S1=$(wp "$T/cp-spec1.json")
|
||
r=$(cpo "$S1"); cchk "150-line range" 0 "$r"
|
||
cin "window cut at 100 lines" "56 more lines: --only 1 --max-lines 160"
|
||
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 100 ] && echo "ok context-pack long window prints 100 lines" || { echo "FAIL context-pack long window"; fail=1; }
|
||
r=$(cpo "$S1" --only 1 --max-lines 160); cchk "--only with the suggested --max-lines" 0 "$r"
|
||
cnot "full window needs no more-lines note" "more lines"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 160 ] && echo "ok context-pack suggested --max-lines is exact" || { echo "FAIL context-pack suggested max: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
# reads, symbols and caller lists
|
||
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec-reads.json" | wrapcp >"$T/cp-spec-reads.json"; SR=$(wp "$T/cp-spec-reads.json")
|
||
r=$(cpo "$SR"); cchk "reads and symbols" 3 "$r"
|
||
cin "a read that moved" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
|
||
cin "a read on a deleted path is GONE" "[r2] c.txt:2-3 GONE (not named by the spec's deletes)"
|
||
cin "a read absent at base is MISSING" "[r3] sub/a.txt:1-2 MISSING (read path absent at base)"
|
||
cin "a present symbol adds no suffix" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
|
||
cin "a missing symbol is named" "[2] b.txt:30-31 MOVED(-2) SYMBOL_MISSING b99"
|
||
cin "an identifier matches whole words only" "MOVED(+3) SYMBOL_MISSING a2 -> a.txt:23-25"
|
||
cin "a non-identifier symbol is a literal substring" "[4] sym.txt:9-9 SAME"
|
||
cnot "the substring symbol is not missing" "SYMBOL_MISSING ::ba"
|
||
cin "the read caption carries its why" "== READ r1: moved below the insertion (a.txt:23-25 MOVED(+3))"
|
||
cin "a GONE read prints its window at base" "== READ r2: deleted path (c.txt:2-3 AT BASE"
|
||
[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack the table lists edits and reads" || { echo "FAIL context-pack table rows with reads"; fail=1; }
|
||
[ "$(grep -n '^== READ r1' "$T/cp.out" | cut -d: -f1)" -gt "$(grep -n '^== \[4\]' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack read windows follow the edit windows" || { echo "FAIL context-pack read window order"; fail=1; }
|
||
[ "$(grep -n '^CALLERS' "$T/cp.out" | head -1 | cut -d: -f1)" -gt "$(grep -n '^== READ r5' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack caller lists follow the read windows" || { echo "FAIL context-pack caller list order"; fail=1; }
|
||
cin "3 callers are all listed" "CALLERS needle_three: 3 hits in 1 files"
|
||
cin "caller hit format" "zcallers.txt:2: needle_three call 2"
|
||
cnot "the hit inside the symbol's own window is dropped" "sym.txt:5: def needle_three"
|
||
cnot "a longer word is no caller" "needle_threefold"
|
||
cin "60 hits print per-file counts" "CALLERS needle_many: 60 hits in 2 files"
|
||
cin "per-file count line" " 30 many1.txt"
|
||
cin "the narrowing command" "narrow: git grep -n -I -w -F -e needle_many -- <path>"
|
||
cnot "60 hits print no hit lines" "many1.txt:1:"
|
||
r=$(cpo "$SR" --only r1); cchk "--only r1 keeps the exit of all anchors" 3 "$r"
|
||
cin "--only r1 prints the read" "== READ r1: moved below the insertion"
|
||
cnot "--only r1 prints no other read" "== READ r2"; cnot "--only r1 prints no edit window" "== [1]"
|
||
r=$(cpo "$SR" --only r9); cchk "--only past the reads" 2 "$r"; grep -qF "context-pack: --only: index outside reads" "$T/cp.err" && echo "ok context-pack names the reads bound" || { echo "FAIL context-pack reads bound"; fail=1; }
|
||
r=$(cpo "$SR" --only 1,r4); cin "--only mixes edit and read labels" "CALLERS needle_many: 60 hits"; cnot "--only lists callers of the selected anchors only" "CALLERS b99"
|
||
r=$(cpo "$SR" --since "$(git -C "$R" rev-parse HEAD)"); cnot "--since HEAD prints no read window" "== READ"; cnot "--since HEAD prints no caller list" "CALLERS"
|
||
r=$(cpo "$SR" --max-lines 3); cchk "--max-lines below the table with reads" 3 "$r"
|
||
[ "$(grep -c '^\[' "$T/cp.out")" = 9 ] && echo "ok context-pack a small budget keeps every edit and read row" || { echo "FAIL context-pack small budget with reads"; fail=1; }
|
||
cnot "a small budget prints no window" "== "; cnot "a small budget prints no caller list" "CALLERS"
|
||
r=$(cpo "$SR" --max-lines 60); cchk "caller lists cut by the budget" 3 "$r"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 60 ] && echo "ok context-pack caller lists stay within --max-lines" || { echo "FAIL context-pack caller lists over budget"; fail=1; }
|
||
cin "the cut caller lists are named" "callers not printed: "
|
||
SUGG=$(grep -o '^callers not printed: .*--max-lines [0-9]*' "$T/cp.out" | grep -o '[0-9]*$')
|
||
r=$(cpo "$SR" --max-lines "$SUGG"); cnot "the suggested --max-lines prints every caller list" "callers not printed"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = "$SUGG" ] && echo "ok context-pack suggested caller --max-lines is exact" || { echo "FAIL context-pack suggested caller max: $(wc -l <"$T/cp.out") vs $SUGG"; fail=1; }
|
||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
|
||
for i in 1 2 3 4 5 6 7 8 9; do echo '{"change": "s'$i'", "location": {"path": "big.txt", "line": '$i', "end_line": '$i', "symbol": "g'$i'"}}'$([ $i -lt 9 ] && echo ,); done
|
||
echo ']}'; } | wrapcp >"$T/cp-spec9sym.json"
|
||
r=$(cpo "$(wp "$T/cp-spec9sym.json")" --max-lines 400); cchk "nine symbols" 0 "$r"
|
||
[ "$(grep -c '^CALLERS' "$T/cp.out")" = 8 ] && echo "ok context-pack at most 8 caller lists" || { echo "FAIL context-pack caller list count"; fail=1; }
|
||
cin "the ninth symbol is named" "callers not listed: g9"
|
||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [], "reads": [{"why": "w", "location": {"path": "a.txt", "line": 20, "end_line": 22, "symbol": "a21"}}]}' | wrapcp >"$T/cp-spec-r0.json"
|
||
r=$(cpo "$(wp "$T/cp-spec-r0.json")"); cchk "a spec of one read, symbol present" 0 "$r"
|
||
cin "a read with a symbol present: no suffix" "[r1] a.txt:20-22 MOVED(+3) -> a.txt:23-25 # w"
|
||
# caller-list bounds: 50 hits list, 51 count; 20 files; a hit's text is cut at 160; CR and bad bytes
|
||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
|
||
n=0; for sy in bound_fifty bound_fiftyone spread_sym long_symbol q.zq sym_bad twenty_sym; do n=$((n+1))
|
||
echo '{"change": "'$sy'", "location": {"path": "anchors.txt", "line": 1, "end_line": 1, "symbol": "'$sy'"}}'$([ $n -lt 7 ] && echo ,); done
|
||
echo ']}'; } | wrapcp >"$T/cp-specb.json"
|
||
r=$(cpo "$(wp "$T/cp-specb.json")" --max-lines 400); cchk "caller list bounds" 0 "$r"
|
||
cin "exactly 50 hits are listed" "CALLERS bound_fifty: 50 hits in 1 files"
|
||
[ "$(grep -c '^bound50.txt:' "$T/cp.out")" = 50 ] && echo "ok context-pack 50 hits print as lines" || { echo "FAIL context-pack 50 hit lines"; fail=1; }
|
||
cin "51 hits are counted" "CALLERS bound_fiftyone: 51 hits in 1 files"
|
||
cin "51 hits: the per-file count" " 51 bound51.txt"
|
||
cnot "51 hits print no hit lines" "bound51.txt:1:"
|
||
grep -qx "(1 more files)" "$T/cp.out" && echo "ok context-pack 21 files: the 21st is named" || { echo "FAIL context-pack 21st file line"; fail=1; }
|
||
grep -qx "narrow: git grep -n -I -F -e q.zq -- <path>" "$T/cp.out" && echo "ok context-pack the narrowing line is exact" || { echo "FAIL context-pack narrowing line"; fail=1; }
|
||
cin "exactly 20 files name no more" "CALLERS twenty_sym: 60 hits in 20 files"
|
||
[ "$(grep -c '(0 more files)' "$T/cp.out")" = 0 ] && echo "ok context-pack 20 files print no more-files line" || { echo "FAIL context-pack 20 files"; fail=1; }
|
||
[ "$(grep -c '^ *[34] manyf/' "$T/cp.out")" = 20 ] && echo "ok context-pack at most 20 files are counted" || { echo "FAIL context-pack file cap: $(grep -c '^ *[34] manyf/' "$T/cp.out")"; fail=1; }
|
||
cin "files are ranked by hits, largest first" " 4 manyf/f21.txt"
|
||
[ "$(grep -n 'manyf/f21.txt' "$T/cp.out" | cut -d: -f1)" -lt "$(grep -n 'manyf/f01.txt' "$T/cp.out" | cut -d: -f1)" ] && echo "ok context-pack the largest file prints first" || { echo "FAIL context-pack file order"; fail=1; }
|
||
cin "a non-identifier narrowing command has no -w" "narrow: git grep -n -I -F -e q.zq -- <path>"
|
||
LL=$(grep '^longline.txt:1: ' "$T/cp.out"); [ "${#LL}" = 176 ] && echo "ok context-pack a hit's text is cut at 160 characters" || { echo "FAIL context-pack hit text length ${#LL}"; fail=1; }
|
||
cnot "a CR in a hit is stripped" "$(printf '\r')"
|
||
cin "a hit with invalid UTF-8 still prints" "bad.txt:1: sym_bad"
|
||
cin "a hit keeps a trailing X and loses its CR" "endX"
|
||
grep -qx "bad.txt:1: sym_bad .* endX" "$T/cp.out" && echo "ok context-pack a hit ends at its last character" || { echo "FAIL context-pack hit end"; fail=1; }
|
||
r=$(cpo "$SR" --only 5); cchk "--only past the last edit anchor" 2 "$r"; grep -qxF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack --only 5 is outside four anchors" || { echo "FAIL context-pack --only 5"; fail=1; }
|
||
r=$(cpo "$SR" --only 4); cchk "--only the last edit anchor" 3 "$r"; cin "--only 4 prints that window" "== [4] sym.txt:9-9"
|
||
r=$(cpo "$SR" --only r6); cchk "--only past the last read" 2 "$r"; grep -qxF "context-pack: --only: index outside reads" "$T/cp.err" && echo "ok context-pack --only r6 is outside five reads" || { echo "FAIL context-pack --only r6"; fail=1; }
|
||
r=$(cpo "$SR" --only r5); cchk "--only the last read" 3 "$r"; cin "--only r5 prints that window" "== READ r5:"
|
||
sed -n 3p "$T/cp.out" | grep -q '^\[r3\]' && echo "ok context-pack a MISSING read sorts with the problem anchors" || { echo "FAIL context-pack MISSING read order"; fail=1; }
|
||
cin "a GONE read checks its symbol at base" "GONE (not named by the spec's deletes) SYMBOL_MISSING c9"
|
||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"location": {"path": "a.txt", "line": 1, "end_line": 2, "symbol": "zz9"}}],'
|
||
echo '"reads": [{"location": {"path": "a.txt", "line": 1, "end_line": 2}}, {"why": "'$(printf '%0250d' 0 | tr 0 w)'", "location": {"path": "a.txt", "line": 3, "end_line": 3}}]}'; } | wrapcp >"$T/cp-specw.json"
|
||
r=$(cpo "$(wp "$T/cp-specw.json")"); cchk "a missing symbol alone fails the run" 3 "$r"
|
||
cin "a symbol absent from an unmoved window" "[1] a.txt:1-2 SAME SYMBOL_MISSING zz9"
|
||
cnot "an anchor without a change or why gets no filler" "XXXX"
|
||
W200=$(printf '%0200d' 0 | tr 0 w); cin "the read caption is cut at 200" "== READ r2: $W200 (a.txt:3-3"
|
||
# the caller lists obey --max-lines at every budget, and the suggested value prints them all
|
||
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
|
||
for i in 1 2 3 4 5 6 7 8 9 10; do echo '{"change": "s'$i'", "location": {"path": "big.txt", "line": '$i', "end_line": '$i', "symbol": "g'$i'"}}'$([ $i -lt 10 ] && echo ,); done
|
||
echo ']}'; } | wrapcp >"$T/cp-spec10sym.json"; S10=$(wp "$T/cp-spec10sym.json")
|
||
r=$(cpo "$S10" --max-lines 400); cin "two symbols past the eighth are named together" "callers not listed: g9,g10"
|
||
FULLN=$(wc -l <"$T/cp.out" | tr -d ' ')
|
||
for b in $((FULLN-1)) $((FULLN-3)) $((FULLN-6)) $((FULLN-9)) $((FULLN-30)); do
|
||
r=$(cpo "$S10" --max-lines "$b")
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le "$b" ] && echo "ok context-pack budget $b holds" || { echo "FAIL context-pack budget $b exceeded: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
SG=$(grep -o '^callers not printed: .*--max-lines [0-9]*' "$T/cp.out" | grep -o '[0-9]*$')
|
||
if [ -n "$SG" ]; then r=$(cpo "$S10" --max-lines "$SG"); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = "$FULLN" ] && ! grep -q 'callers not printed\|not expanded' "$T/cp.out" && echo "ok context-pack budget $b: the suggestion prints everything" || { echo "FAIL context-pack budget $b: suggestion $SG gave $(wc -l <"$T/cp.out") of $FULLN"; fail=1; }; fi
|
||
done
|
||
r=$(cpo "$S10" --max-lines $((FULLN-1))); [ "$(grep -c '^CALLERS' "$T/cp.out")" = 6 ] && echo "ok context-pack one line short drops two lists for the note" || { echo "FAIL context-pack one line short: $(grep -c '^CALLERS' "$T/cp.out") lists"; fail=1; }
|
||
cin "the dropped lists are named" "callers not printed: g7,g8: --max-lines $FULLN"
|
||
# window budgets: the exact thresholds, and what the caller lists leave of them
|
||
r=$(cpo "$S1" --max-lines 104); cnot "104 lines expand no 100-line window" "== ["; cin "104 lines skip a 100-line window" "not expanded: --only 1"
|
||
r=$(cpo "$S1" --max-lines 105); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && grep -qx "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack 105 lines hold a capped window and its note" || { echo "FAIL context-pack 105-line budget"; fail=1; }
|
||
S30=$(wp "$T/cp-spec30.json")
|
||
r=$(cpo "$S30" --max-lines 186); [ "$(grep -c '^== ' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 186 ] && echo "ok context-pack 186 lines hold 9 windows and the note" || { echo "FAIL context-pack 186-line budget"; fail=1; }
|
||
r=$(cpo "$S30" --max-lines 185); [ "$(grep -c '^== ' "$T/cp.out")" = 8 ] && echo "ok context-pack 185 lines hold 8 windows" || { echo "FAIL context-pack 185-line budget"; fail=1; }
|
||
r=$(cpo "$S30" --max-lines 542); [ "$(grep -c '^== ' "$T/cp.out")" = 30 ] && echo "ok context-pack 542 lines hold all 30 windows" || { echo "FAIL context-pack 542-line budget"; fail=1; }; cnot "542 lines need no not-expanded note" "not expanded"
|
||
for b in 530 531 532 533 534 535 536 537 538 539 540 541; do
|
||
r=$(cpo "$S30" --max-lines $b); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le $b ] && grep -q '^not expanded: --only 30$' "$T/cp.out" && echo "ok context-pack budget $b skips only the last window" || { echo "FAIL context-pack budget $b: $(wc -l <"$T/cp.out") lines"; fail=1; }
|
||
done
|
||
for b in 37 38 39 40 41 42 43 44 45 46 47; do
|
||
r=$(cpo "$S30" --only 1,2 --max-lines $b); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = $b ] && [ "$(grep -c '^== ' "$T/cp.out")" = 1 ] && grep -q 'more lines: --only 1 ' "$T/cp.out" && echo "ok context-pack --only 1,2 at $b lines truncates window 1 to fit" || { echo "FAIL context-pack --only 1,2 at $b: $(wc -l <"$T/cp.out") lines"; fail=1; }
|
||
done
|
||
r=$(cpo "$S30" --only 1,2 --max-lines 36); cnot "--only 1,2 at 36 lines expands nothing" "== ["
|
||
r=$(cpo "$S10" --max-lines 94); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 94 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 10 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 6 ] && echo "ok context-pack one line short keeps every window and 6 lists" || { echo "FAIL context-pack 94-line S10"; fail=1; }
|
||
r=$(cpo "$S10" --max-lines 86); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 86 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 9 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 5 ] && echo "ok context-pack 86 lines: 9 windows, 5 lists, every note" || { echo "FAIL context-pack 86-line S10"; fail=1; }
|
||
r=$(cpo "$S10" --max-lines 65); [ "$(grep -c '^== ' "$T/cp.out")" = 7 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 0 ] && grep -q '^callers not printed: ' "$T/cp.out" && grep -q '^callers not listed: g9,g10' "$T/cp.out" && echo "ok context-pack windows outrank caller lists" || { echo "FAIL context-pack 65-line S10"; fail=1; }
|
||
r=$(cpo "$SR" --max-lines 40); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 40 ] && [ "$(grep -c '^== ' "$T/cp.out")" = 3 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 2 ] && echo "ok context-pack 40 lines: 3 windows, 2 lists, no line unused" || { echo "FAIL context-pack 40-line SR"; fail=1; }
|
||
r=$(cpo "$SR" --max-lines 44); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 44 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 3 ] && echo "ok context-pack 44 lines: 3 lists" || { echo "FAIL context-pack 44-line SR"; fail=1; }
|
||
r=$(cpo "$SR" --max-lines 62); [ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 61 ] && [ "$(grep -c '^CALLERS' "$T/cp.out")" = 4 ] && echo "ok context-pack 62 lines: a list that needs 2 lines waits" || { echo "FAIL context-pack 62-line SR"; fail=1; }
|
||
# usage and the spec document (a saved file or stdin); no error echoes an input value
|
||
r=$(cpo "$SP" --max-lines CANARY-usage); cchk "bad --max-lines" 2 "$r"; cnot "usage error echoes no value" "CANARY-usage"
|
||
UNREADABLE="context-pack: spec: unreadable; pipe huginn view --raw <id> or pass a saved file"
|
||
r=$(cpo "$T/CANARY-nofile.json"); cchk "unreadable spec file" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the unreadable spec" || { echo "FAIL context-pack unreadable message: $(cat "$T/cp.err")"; fail=1; }
|
||
cnot "unreadable spec echoes no path" "CANARY"
|
||
r=$(cpo "$SP"); cp "$T/cp.out" "$T/cp.want"
|
||
r=$($PY "$CP" - "$Rw" <"$T/cp-spec.json" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "spec from stdin" 3 "$r"; [ "$r" = 3 ] || cat "$T/cp.err"
|
||
cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack stdin equals the file run" || { echo "FAIL context-pack stdin differs from the file run"; fail=1; }
|
||
r=$(: | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "empty stdin" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the empty spec" || { echo "FAIL context-pack empty stdin message"; fail=1; }
|
||
r=$(echo 'absent t:cut_spec:CANARY-x' | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "an absent line" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the absent spec" || { echo "FAIL context-pack absent message"; fail=1; }
|
||
cnot "an absent line echoes no id" "CANARY"
|
||
echo '{}' >"$T/cp-empty.json"; r=$(cpo "$(wp "$T/cp-empty.json")"); cchk "a spec that is {}" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the empty object" || { echo "FAIL context-pack {} message"; fail=1; }
|
||
printf '' | cat - "$T/cp-spec.json" >"$T/cp-bom.json"; r=$(cpo "$(wp "$T/cp-bom.json")"); cchk "a spec file with a UTF-8 BOM" 3 "$r"
|
||
r=$(cat "$T/cp-bom.json" | $PY "$CP" - "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "a spec on stdin with a UTF-8 BOM" 3 "$r"
|
||
$PY -c 'import json,sys; v=json.load(open(sys.argv[1])); v["kind"]="ruling"; json.dump(v,open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-notspec.json")"
|
||
r=$(cpo "$(wp "$T/cp-notspec.json")"); cchk "a well-shaped document of another kind" 4 "$r"
|
||
$PY -c 'import json,sys; json.dump(json.load(open(sys.argv[1]))["value"],open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-bare.json")"
|
||
r=$(cpo "$(wp "$T/cp-bare.json")"); cchk "a bare value with no kind" 4 "$r"
|
||
for f in campaign cut revision; do
|
||
$PY -c 'import json,sys; v=json.load(open(sys.argv[1])); del v["value"][sys.argv[3]]; json.dump(v,open(sys.argv[2],"w"))' "$(wp "$T/cp-spec.json")" "$(wp "$T/cp-noid.json")" $f
|
||
r=$(cpo "$(wp "$T/cp-noid.json")"); cchk "a spec without $f" 4 "$r"; cnot "no None for a missing $f" "None"
|
||
done
|
||
echo '{"kind": "ruling", "value": {"title": "CANARY-title"}}' >"$T/cp-ruling.json"; r=$(cpo "$(wp "$T/cp-ruling.json")"); cchk "a document that is no cut_spec" 4 "$r"; grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack names the foreign document" || { echo "FAIL context-pack foreign document message"; fail=1; }
|
||
cnot "a foreign document echoes no value" "CANARY"
|
||
grep -qF "usage: " "$T/cp.err" || echo "ok context-pack spec error prints no usage text"
|
||
r=$($PY "$CP" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "usage: no spec source" 2 "$r"
|
||
grep -qF "context-pack: usage error" "$T/cp.err" && echo "ok context-pack usage error text" || { echo "FAIL context-pack usage text"; fail=1; }
|
||
r=$($PY "$CP" "$SP" "$T/CANARY-notrepo" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "worktree is not a repo" 2 "$r"; grep -qF "context-pack: worktree: not a git worktree" "$T/cp.err" && echo "ok context-pack names the worktree" || { echo "FAIL context-pack worktree message"; fail=1; }; cnot "worktree error echoes no path" "CANARY-notrepo"
|
||
# glob characters in a spec path name that file only; straddling hunks; --since boundaries
|
||
R2=$T/cp2; mkdir -p "$R2/src/[a]" "$R2/src/a"; R2w=$(wp "$R2")
|
||
g2() { git -C "$R2" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
|
||
g2 init -q
|
||
seqf x 30 >"$R2/src/[a]/f.txt"; seqf x 30 >"$R2/src/a/f.txt"
|
||
for n in s t u v; do seqf x 30 >"$R2/$n.txt"; done; seqf w 12 >"$R2/w.txt"; seqf g 200 >"$R2/big.txt"; echo q1 >"$R2/one.txt"
|
||
g2 add -A; g2 commit -q -m base; B2=$(git -C "$R2" rev-parse HEAD)
|
||
{ printf 'new1\nnew2\n'; cat "$R2/src/a/f.txt"; } >"$R2/src/a/f.new"; mv "$R2/src/a/f.new" "$R2/src/a/f.txt"
|
||
sed -i 8,12d "$R2/s.txt"; sed -i 15,25d "$R2/t.txt"
|
||
sed -i '8,12c\NEW' "$R2/u.txt"; sed -i '15,25c\NEW' "$R2/v.txt"
|
||
g2 add -A; g2 commit -q -m second; S2=$(git -C "$R2" rev-parse HEAD)
|
||
sed -i 's/^w5$/w5-EDITED/' "$R2/w.txt"
|
||
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
|
||
{"change": "glob path", "location": {"path": "src/[a]/f.txt", "line": 10, "end_line": 12}},
|
||
{"change": "pure deletion above the start", "location": {"path": "s.txt", "line": 10, "end_line": 20}},
|
||
{"change": "pure deletion past the end", "location": {"path": "t.txt", "line": 10, "end_line": 20}},
|
||
{"change": "one-line replacement above the start", "location": {"path": "u.txt", "line": 10, "end_line": 20}},
|
||
{"change": "one-line replacement past the end", "location": {"path": "v.txt", "line": 10, "end_line": 20}},
|
||
{"change": "edit on the last line", "location": {"path": "w.txt", "line": 3, "end_line": 5}},
|
||
{"change": "edit on the first line", "location": {"path": "w.txt", "line": 5, "end_line": 7}},
|
||
{"change": "after the edit", "location": {"path": "w.txt", "line": 6, "end_line": 8}},
|
||
{"change": "before the edit", "location": {"path": "w.txt", "line": 1, "end_line": 4}}]}' | wrapcp >"$T/cp-spec2.json"
|
||
SP2=$(wp "$T/cp-spec2.json")
|
||
cp2() { local s=$1; shift; $PY "$CP" "$s" "$R2w" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
|
||
r=$(cp2 "$SP2"); cchk "straddle fixture" 0 "$r"
|
||
cin "a glob path reads its own file: SAME" "[1] src/[a]/f.txt:10-12 SAME -> src/[a]/f.txt:10-12"
|
||
cin "pure deletion starting above the anchor starts at the first survivor" "[2] s.txt:10-20 TOUCHED(+0/-5 inside) -> s.txt:8-15"
|
||
cin "pure deletion ending past the anchor ends at the last survivor" "[3] t.txt:10-20 TOUCHED(+0/-11 inside) -> t.txt:10-14"
|
||
cin "one-line replacement above the start begins at the replacement" "[4] u.txt:10-20 TOUCHED(+1/-5 inside) -> u.txt:8-16"
|
||
cin "one-line replacement past the end ends at the replacement" "[5] v.txt:10-20 TOUCHED(+1/-11 inside) -> v.txt:10-15"
|
||
cin "straddle window starts at the mapped range" "== [2] s.txt:8-15"
|
||
r=$(cp2 "$SP2" --since "$S2" --only 6,7,8,9)
|
||
cin "--since: an edit on the anchor's last line is inside" "== [6] w.txt:3-5"
|
||
cin "--since: an edit on the anchor's first line is inside" "== [7] w.txt:5-7"
|
||
cnot "--since: an anchor after the edit has no window" "== [8] "
|
||
cnot "--since: an anchor before the edit has no window" "== [9] "
|
||
cin "--since keeps every anchor in the table" "[9] w.txt:1-4 SAME"
|
||
# a --since commit whose tree is unreadable: git diff fails, so no anchor is claimed edited
|
||
BL=$(echo only | git -C "$R2" hash-object -w --stdin); TR=$(printf '100644 blob %s\tonly.txt\n' "$BL" | git -C "$R2" mktree)
|
||
CM=$(git -C "$R2" -c user.name=t -c user.email=t@t commit-tree "$TR" -m orphan)
|
||
chmod u+w "$R2/.git/objects/${TR:0:2}/${TR:2}" 2>/dev/null; rm -f "$R2/.git/objects/${TR:0:2}/${TR:2}"
|
||
r=$(cp2 "$SP2" --since "$CM"); cchk "--since with an unreadable tree" 0 "$r"
|
||
cnot "--since: a failed git diff claims no edited window" "== ["
|
||
cin "--since: a failed git diff keeps the table" "[6] w.txt:3-5"
|
||
# a spec value of the wrong type is a typed refusal naming the field, never a traceback or the value
|
||
$PY - "$T" "$SP" <<'PYE'
|
||
import copy, json, sys
|
||
t, sp = sys.argv[1], sys.argv[2]
|
||
spec = json.load(open(sp, encoding="utf-8"))
|
||
cases = {
|
||
"line": (lambda v: v["file_changes"][1]["location"].update(line="CANARY-line"), "file_changes[2].location.line"),
|
||
"line-bool": (lambda v: v["file_changes"][0]["location"].update(line=True), "file_changes[1].location.line"),
|
||
"end_line": (lambda v: v["file_changes"][2]["location"].update(end_line="CANARY-end"), "file_changes[3].location.end_line"),
|
||
"path": (lambda v: v["file_changes"][0]["location"].update(path=["CANARY-path"]), "file_changes[1].location.path"),
|
||
"nolocation": (lambda v: v["file_changes"][3].pop("location"), "file_changes[4].location"),
|
||
"location-str": (lambda v: v["file_changes"][0].update(location="CANARY-loc"), "file_changes[1].location"),
|
||
"entry-str": (lambda v: v["file_changes"].__setitem__(0, "CANARY-str"), "file_changes[1]"),
|
||
"change": (lambda v: v["file_changes"][0].update(change={"CANARY": 1}), "file_changes[1].change"),
|
||
"fcs-str": (lambda v: v.update(file_changes="CANARY-fcs"), "file_changes"),
|
||
"base": (lambda v: v.update(base=12345), "base"),
|
||
"deletes-str": (lambda v: v.update(deletes="CANARY-del"), "deletes"),
|
||
"deletes-nopath": (lambda v: v.update(deletes=[{"note": "CANARY-note"}]), "deletes[1].path"),
|
||
"deletes-entry-str": (lambda v: v.update(deletes=["CANARY-d"]), "deletes[1].path"),
|
||
"reads-str": (lambda v: v.update(reads="CANARY-reads"), "reads"),
|
||
"read-entry-str": (lambda v: v.update(reads=["CANARY-read"]), "reads[1]"),
|
||
"read-nolocation": (lambda v: v.update(reads=[{"why": "w"}]), "reads[1].location"),
|
||
"read-path": (lambda v: v.update(reads=[{"why": "w", "location": {"path": ["CANARY-p"], "line": 1}}]), "reads[1].location.path"),
|
||
"read-line": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": "CANARY-l"}}]), "reads[1].location.line"),
|
||
"read-why": (lambda v: v.update(reads=[{"why": {"CANARY": 1}, "location": {"path": "a.txt", "line": 1}}]), "reads[1].why"),
|
||
"read-symbol": (lambda v: v.update(reads=[{"why": "w", "location": {"path": "a.txt", "line": 1, "symbol": 7}}]), "reads[1].location.symbol"),
|
||
"edit-symbol": (lambda v: v["file_changes"][0]["location"].update(symbol=["CANARY-s"]), "file_changes[1].location.symbol"),
|
||
}
|
||
names = []
|
||
for name, (edit, field) in cases.items():
|
||
d = copy.deepcopy(spec)
|
||
edit(d["value"])
|
||
json.dump(d, open("%s/cp-bad-%s.json" % (t, name), "w"))
|
||
names.append("%s %s" % (name, field))
|
||
open(t + "/cp-bad.list", "w", newline=chr(10)).write("\n".join(names) + "\n")
|
||
PYE
|
||
while read -r nm fld; do
|
||
r=$(cpo "$(wp "$T/cp-bad-$nm.json")"); cchk "wrong-typed spec: $nm" 4 "$r"
|
||
grep -qF "context-pack: spec: $fld: InvalidInput" "$T/cp.err" && echo "ok context-pack names $fld" || { echo "FAIL context-pack message for $nm: $(cat "$T/cp.err")"; fail=1; }
|
||
cnot "wrong-typed spec $nm: no value" "CANARY"; cnot "wrong-typed spec $nm: no traceback" "Traceback"
|
||
done <"$T/cp-bad.list"
|
||
echo '{"base": "CANARY-base", "deletes": [], "file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1, "end_line": 2}}]}' | wrapcp >"$T/cp-specnb.json"
|
||
r=$(cpo "$(wp "$T/cp-specnb.json")"); cchk "a base that is no sha" 3 "$r"
|
||
cin "a base that is no sha reads UNRESOLVED" "UNRESOLVED base not a commit here"
|
||
cin "a base that is no sha is not echoed in the header" "base unresolvable HEAD"
|
||
echo '{"file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1}}]}' | wrapcp >"$T/cp-spec-nobase.json"
|
||
r=$(cpo "$(wp "$T/cp-spec-nobase.json")"); cchk "a spec value without a base" 4 "$r"
|
||
grep -qxF "$UNREADABLE" "$T/cp.err" && echo "ok context-pack a missing base is a typed refusal" || { echo "FAIL context-pack missing base: $(cat "$T/cp.err")"; fail=1; }
|
||
# a --max-lines below the table: the table always lists every anchor and no window prints
|
||
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 5); cchk "--max-lines below the table" 0 "$r"
|
||
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack a small budget keeps the full table" || { echo "FAIL context-pack small budget table"; fail=1; }
|
||
cnot "a small budget prints no window" "== ["
|
||
[ -z "$(sed -n 32p "$T/cp.out")" ] && echo "ok context-pack a blank line follows the table" || { echo "FAIL context-pack blank line after the table"; fail=1; }
|
||
cin "a small budget lists every window as not expanded" "not expanded: --only 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30"
|
||
# --only truncates a window to the budget and says how to see the rest
|
||
r=$(cpo "$S1" --only 1 --max-lines 50); cchk "--only truncation" 0 "$r"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only fills the budget exactly" || { echo "FAIL context-pack --only budget: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 45 ] && echo "ok context-pack --only truncated window has 45 lines" || { echo "FAIL context-pack --only window lines"; fail=1; }
|
||
cin "--only truncation note counts the rest" "111 more lines: --only 1 --max-lines 160"
|
||
r=$(cpo "$S1" --only 1 --max-lines 7); cchk "--only at the smallest truncatable budget" 0 "$r"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 7 ] && [ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 2 ] && echo "ok context-pack --only budget 7 prints 2 window lines" || { echo "FAIL context-pack --only budget 7"; fail=1; }
|
||
r=$(cpo "$S1" --only 1 --max-lines 6); cin "--only below the smallest truncation is not expanded" "not expanded: --only 1"
|
||
cnot "--only below the smallest truncation prints no window" "== ["
|
||
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
|
||
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
|
||
{"change": "short", "location": {"path": "big.txt", "line": 200, "end_line": 210}}]}' | wrapcp >"$T/cp-spec2w.json"
|
||
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 50); cchk "--only two windows" 0 "$r"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only keeps room for the not-expanded line" || { echo "FAIL context-pack --only reserve: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
cin "--only: the window that no longer fits is listed" "not expanded: --only 2"
|
||
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 161); cchk "--only first window needs exactly the remaining budget" 0 "$r"
|
||
cin "--only: a window that fills the budget minus the reserve is cut, not skipped" "== [1] big.txt:20-169"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 161 ] && echo "ok context-pack --only cut window keeps the reserve line" || { echo "FAIL context-pack --only exact need: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
r=$(cpo "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 8); cchk "--only two windows, too small to cut" 0 "$r"
|
||
cnot "--only: below four lines after the reserve nothing is cut" "== ["
|
||
cin "--only: both are listed as not expanded" "not expanded: --only 1,2"
|
||
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
|
||
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
|
||
{"change": "tiny", "location": {"path": "one.txt", "line": 1, "end_line": 1}}]}' | wrapcp >"$T/cp-spec3.json"
|
||
r=$(cp2 "$(wp "$T/cp-spec3.json")" --only 1,2 --max-lines 100); cchk "--only cut window then a tiny one" 0 "$r"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 100 ] && echo "ok context-pack a cut window is charged what it prints" || { echo "FAIL context-pack cut window charge: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
cin "a cut window leaves no room for the next one" "not expanded: --only 2"
|
||
r=$(cpo "$S1" --max-lines 104); cin "a 100-line cut window needs 102 lines of budget" "not expanded: --only 1"
|
||
r=$(cpo "$S1" --max-lines 105); cin "a 100-line cut window fits in 105 lines" "56 more lines: --only 1 --max-lines 160"
|
||
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && echo "ok context-pack a 100-line cut window fills 105 lines" || { echo "FAIL context-pack cut window at 105: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
grep -qxF "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack the more-lines note is its own line" || { echo "FAIL context-pack more-lines note line"; fail=1; }
|
||
r=$(cpo "$(wp "$T/cp-spec30.json")" --max-lines 202); cchk "reserve line at the budget edge" 0 "$r"
|
||
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 202 ] && echo "ok context-pack a window that would crowd out the not-expanded line is skipped" || { echo "FAIL context-pack reserve at 202: $(wc -l <"$T/cp.out")"; fail=1; }
|
||
# standard library plus git only
|
||
if grep -qE '^import (requests|yaml|tree_sitter)|^from (requests|yaml|tree_sitter)|ctags|tree-sitter|"rg"' "$CP"; then echo "FAIL context-pack: a dependency beyond git"; fail=1; else echo "ok context-pack: standard library and git only"; fi
|
||
|
||
# ---- ygg-verify.sh transport switch: the local branch (this host is Yggdrasil) and, under a mock
|
||
# ssh that runs the remote command string in a shell with a fake remote HOME, the remote branch.
|
||
# Docker is the mutation-mock fake; its container is a directory and does not mount the work
|
||
# directory, so these jobs prove the transport, slots, clone, verdict and summary, not a toolchain.
|
||
YL=$T/yl; mkdir -p "$YL/bin" "$YL/rhome"
|
||
cp "$here/fixtures/mutation-mock/fakebin/"{docker,sudo,flock} "$YL/bin/"
|
||
printf '#!/usr/bin/env bash\necho "ssh $*" >>"$YL_SSHLOG"\nwhile [ "$1" = -o ]; do shift 2; done\nshift\nHOME=$YL_RHOME exec bash -c "$*"\n' >"$YL/bin/ssh"
|
||
printf '#!/usr/bin/env bash\necho "scp $*" >>"$YL_SSHLOG"\nwhile [ "$1" = -q ]; do shift; done\nsrc=$1; dst=${2#*:}; cp "$src" "$YL_RHOME/$dst"\n' >"$YL/bin/scp"
|
||
printf '#!/usr/bin/env bash\necho "${YL_HOSTNAME:-starfire}"\n' >"$YL/bin/hostname"
|
||
chmod +x "$YL/bin/"*
|
||
YLR=$YL/repo; mkdir -p "$YLR"; git -C "$YLR" init -q; echo marker >"$YLR/m.txt"
|
||
git -C "$YLR" add m.txt; git -C "$YLR" -c user.name=t -c user.email=t@t commit -qm m
|
||
ylrun() { # ylrun <extra env assignments...> -- <args>: ygg-verify under the mocks; sets yl_out yl_st
|
||
local envs=(); while [ "$1" != -- ]; do envs+=("$1"); shift; done; shift
|
||
: >"$YL/ssh.log"
|
||
yl_out=$(env PATH="$YL/bin:$PATH" FAKE_DOCKER="$YL/fd" YL_SSHLOG="$YL/ssh.log" YL_RHOME="$YL/rhome" YGG_VERIFY_ROOT="$YL/root" \
|
||
TIMEOUT=60 "${envs[@]}" bash "$V" "$@" 2>&1); yl_st=$?
|
||
}
|
||
ylchk() { # ylchk <label> <expected status> <must-contain> <ssh used: 0|1>
|
||
local used=0; [ -s "$YL/ssh.log" ] && used=1
|
||
if [ "$yl_st" != "$2" ]; then echo "FAIL $1: exit $yl_st, wanted $2: $yl_out"; fail=1
|
||
elif ! printf '%s\n' "$yl_out" | grep -qF -- "$3"; then echo "FAIL $1: missing: $3 in: $yl_out"; fail=1
|
||
elif [ "$used" != "$4" ]; then echo "FAIL $1: ssh used=$used, wanted $4: $(cat "$YL/ssh.log")"; fail=1
|
||
else echo "ok $1"; fi
|
||
}
|
||
ylrun YGG_VERIFY_LOCAL=1 -- "$YLR" HEAD base 'echo localjob-ran; exit 3'
|
||
ylchk "ygg-verify local: the job's status and output come back, no ssh" 3 "localjob-ran" 0
|
||
[ -d "$YL/root/repos/repo.git" ] && [ -z "$(ls "$YL/root/work" | grep -v '\.killed$')" ] && echo "ok ygg-verify local: mirror under YGG_VERIFY_ROOT, work directory removed" || { echo "FAIL ygg-verify local: mirror or work dir state: $(ls -R "$YL/root" | head)"; fail=1; }
|
||
ylrun YGG_VERIFY_LOCAL=1 RUN_ON_HOST=1 -- "$YLR" HEAD base 'cat m.txt; echo "root=$YGG_ROOT slots=$YGG_SLOTS"; test -e "$YGG_WORK/m.txt"'
|
||
ylchk "ygg-verify local: RUN_ON_HOST runs in the checked-out revision with its environment" 0 "root=$YL/root slots=5" 0
|
||
ylrun YGG_VERIFY_LOCAL=1 SUMMARY="$YL/s.log" -- "$YLR" HEAD base 'echo "test result: ok. 4 passed; 0 failed; 0 ignored"'
|
||
ylchk "ygg-verify local: SUMMARY prints the bounded summary" 0 "FRAMEWORK cargo passed=4 failed=0" 0
|
||
out=$(WAIT_MAX=20 SETTLE=0 bash "$V" wait "$YL/s.log"); st=$?
|
||
[ "$st" = 0 ] && printf '%s\n' "$out" | grep -qF "FRAMEWORK cargo passed=4" && echo "ok ygg-verify local: wait returns the finished job" || { echo "FAIL ygg-verify local wait: $st $out"; fail=1; }
|
||
# all slots held by other jobs: the job waits, then takes the slot that frees (real flock on the slot files)
|
||
( exec 8>"$YL/root/slot-1.lock"; flock -n 8; sleep 7 ) & sb=$!
|
||
sleep 1
|
||
ylrun YGG_VERIFY_LOCAL=1 SLOTS=1 -- "$YLR" HEAD base 'echo slotted'
|
||
wait "$sb"
|
||
ylchk "ygg-verify local: waits for a busy slot, then runs" 0 "all 1 slots busy; waiting" 0
|
||
# detection: hostname yggdrasil-candidate is local without the env; another host, or YGG_VERIFY_LOCAL=0, goes over ssh
|
||
ylrun YL_HOSTNAME=yggdrasil-candidate -- "$YLR" HEAD base 'echo auto-local'
|
||
ylchk "ygg-verify detection: hostname yggdrasil-candidate runs locally" 0 "auto-local" 0
|
||
ylrun YL_HOSTNAME=Yggdrasil -- "$YLR" HEAD base 'echo auto-local2'
|
||
ylchk "ygg-verify detection: hostname Yggdrasil runs locally" 0 "auto-local2" 0
|
||
ylrun YL_HOSTNAME=yggdrasil-candidate YGG_VERIFY_LOCAL=0 -- "$YLR" HEAD base 'echo forced-remote; exit 5'
|
||
ylchk "ygg-verify detection: YGG_VERIFY_LOCAL=0 forces ssh (mock ssh: remote command string in a fake HOME)" 5 "forced-remote" 1
|
||
ylrun YL_HOSTNAME=starfire -- "$YLR" HEAD base 'echo workstation'
|
||
ylchk "ygg-verify detection: another hostname uses ssh" 0 "workstation" 1
|
||
[ -d "$YL/rhome/eureka-verify/repos/repo.git" ] && echo "ok ygg-verify remote (mock ssh): mirror pushed to the remote ~/eureka-verify" || { echo "FAIL ygg-verify remote mirror missing: $(cat "$YL/ssh.log")"; fail=1; }
|
||
ylrun YL_HOSTNAME=yggdrasil-candidate YGG_HOST=ygg -- "$YLR" HEAD base 'echo explicit-host'
|
||
ylchk "ygg-verify detection: an explicit YGG_HOST keeps the ssh path" 0 "explicit-host" 1
|
||
ylrun YGG_VERIFY_LOCAL=maybe -- "$YLR" HEAD base 'true'
|
||
ylchk "ygg-verify detection: a bad YGG_VERIFY_LOCAL value is refused" 2 "must be 0, 1 or unset" 0
|
||
exit $fail
|