Add tools/wait-on.sh: one waiter for pids, logs and files
Same contract as ygg-verify.sh wait (270 s cap, bounded tail, RUNNING exit 99, exit 98 for a thing gone with no completion evidence). Windows PIDs are found through tasklist, since kill -0 only knows Git Bash's own. Fixtures in tools/test-tools.sh; INDEX row added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0bb414d927
commit
a886d4524a
3 changed files with 102 additions and 0 deletions
|
|
@ -9,6 +9,7 @@ gets a Soul pass before merge.
|
|||
| script | purpose | consumer | named by |
|
||||
|---|---|---|---|
|
||||
| `tools/stopgap/ygg-verify.sh` | Pushes an exact revision to a mirror on Yggdrasil and runs one command in a capped, niced container (`RUN_ON_HOST=1`: run a host-side command in the same slot, clone and watchdog instead; the watchdog kills every container carrying the job label). `SUMMARY=<log>` keeps the full log and prints a bounded summary (verdict, counts, ZERO TESTS RAN, failing tests); `wait <log>` blocks at most 270 s and returns the summary once; call it with a Bash tool `timeout` of at least 280000 ms. Dies with the stopgap when Idunn's verify transaction lands. | Hands, Soul, Imagination probes | `references/briefs.md` (Hands long jobs and verification, Soul long jobs), `SKILL.md` (Yggdrasil) |
|
||||
| `tools/wait-on.sh` | One general waiter for anything that is not a Yggdrasil job: `pid <pid> [<log>]`, `log <log> <done-regex>` (with `WAIT_PID=<pid>` a dead process with no match is exit 98), `file <path>`. Same contract as `ygg-verify.sh wait`: blocks at most 270 s, exit 0 done with a tail of at most 60 log lines, `RUNNING <n>s` exit 99, exit 98 gone with no completion evidence; call it with a Bash tool `timeout` of at least 280000 ms. Replaces hand-rolled poll loops and idle `true`/`echo`/`sleep` calls. | Hands, Soul, Imagination probes | `references/briefs.md` (Hands long jobs, Soul long jobs, detached scripts) |
|
||||
| `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL <file> [ <file> ]`, jest `FAIL <file>` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) |
|
||||
| `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them |
|
||||
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
|
||||
|
|
|
|||
|
|
@ -191,4 +191,31 @@ check "$(seg jest-real.log load)" 1 "FRAMEWORK jest passed=0 failed=0 skipped=0"
|
|||
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)"
|
||||
|
||||
# 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"
|
||||
r=$(wo bogus); wchk "bad usage" 2 "$r"
|
||||
wait
|
||||
exit $fail
|
||||
|
|
|
|||
74
tools/wait-on.sh
Normal file
74
tools/wait-on.sh
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
# One general waiter, so no agent hand-rolls a poll loop. Same contract as
|
||||
# `ygg-verify.sh wait`: one call blocks at most 270 s (WAIT_MAX can only lower
|
||||
# that), checks locally every 2 s (no model turn), and exits:
|
||||
# 0 done; prints a bounded tail (at most 60 lines) of the log when one is given
|
||||
# 99 still going; prints `RUNNING <n>s (...)`: call it again
|
||||
# 98 the thing waited on is gone with no completion evidence
|
||||
# Call it with a Bash tool `timeout` of at least 280000 ms.
|
||||
#
|
||||
# Forms:
|
||||
# wait-on.sh pid <pid> [<log>] done when the process is gone (exit 0)
|
||||
# wait-on.sh log <log> <done-regex> done when a log line matches the ERE
|
||||
# wait-on.sh file <path> done when the file exists and is non-empty
|
||||
# `log` accepts WAIT_PID=<pid>: if that process dies and the regex never matched
|
||||
# (checked once more after the death), exit 98. Without WAIT_PID a log that does
|
||||
# not exist after 30 s is exit 98 (never started). `pid` cannot tell a finished
|
||||
# process from one that never existed: both are exit 0, so pass a log and read it.
|
||||
#
|
||||
# PIDs: on Git Bash a Windows PID (PowerShell Start-Process .Id, a task manager
|
||||
# number) is seen by `tasklist`, but `kill -0` does not know it; Git Bash's own
|
||||
# `$!` is known to `kill -0` and also to `tasklist`'s WINPID only by luck. Alive
|
||||
# means either check says so. Linux uses `kill -0`.
|
||||
set -uo pipefail
|
||||
mode=${1:-}; cap=${WAIT_MAX:-270}; [ "$cap" -le 270 ] || cap=270
|
||||
t0=$(date +%s)
|
||||
usage() { echo "usage: wait-on.sh pid <pid> [<log>] | log <log> <done-regex> | file <path>" >&2; exit 2; }
|
||||
|
||||
alive() {
|
||||
local p=$1
|
||||
case "$p" in ''|*[!0-9]*) return 1 ;; esac
|
||||
kill -0 "$p" 2>/dev/null && return 0
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
tasklist //FI "PID eq $p" //NH 2>/dev/null | tr -d '\r' | grep -qE "[[:space:]]$p[[:space:]]" && return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
tail60() { [ -n "${1:-}" ] && [ -f "$1" ] && tail -n 60 "$1" | cut -c1-400; return 0; }
|
||||
running() { echo "RUNNING $(( $(date +%s) - t0 ))s ($1)"; exit 99; }
|
||||
elapsed() { echo $(( $(date +%s) - t0 )); }
|
||||
|
||||
case "$mode" in
|
||||
pid)
|
||||
pid=${2:-}; log=${3:-}; [ -n "$pid" ] || usage
|
||||
while :; do
|
||||
if ! alive "$pid"; then tail60 "$log"; exit 0; fi
|
||||
[ "$(elapsed)" -lt "$cap" ] || running "pid $pid alive; last log line: $(tail -n 1 "$log" 2>/dev/null | cut -c1-160)"
|
||||
sleep 2
|
||||
done ;;
|
||||
log)
|
||||
log=${2:-}; re=${3:-}; [ -n "$log" ] && [ -n "$re" ] || usage
|
||||
wp=${WAIT_PID:-}
|
||||
while :; do
|
||||
if [ -f "$log" ] && grep -Eq -- "$re" "$log" 2>/dev/null; then tail60 "$log"; exit 0; fi
|
||||
if [ -n "$wp" ] && ! alive "$wp"; then
|
||||
sleep 1
|
||||
if [ -f "$log" ] && grep -Eq -- "$re" "$log" 2>/dev/null; then tail60 "$log"; exit 0; fi
|
||||
echo "wait-on: DEAD - pid $wp is gone and /$re/ never matched in $log"; tail60 "$log"; exit 98
|
||||
fi
|
||||
if [ -z "$wp" ] && [ ! -f "$log" ] && [ "$(elapsed)" -ge 30 ]; then
|
||||
echo "wait-on: NOT STARTED - no $log after 30 s"; exit 98
|
||||
fi
|
||||
[ "$(elapsed)" -lt "$cap" ] || running "no /$re/ yet; last line: $(tail -n 1 "$log" 2>/dev/null | cut -c1-160)"
|
||||
sleep 2
|
||||
done ;;
|
||||
file)
|
||||
path=${2:-}; [ -n "$path" ] || usage
|
||||
while :; do
|
||||
[ -s "$path" ] && exit 0
|
||||
[ "$(elapsed)" -lt "$cap" ] || running "$path not there yet"
|
||||
sleep 2
|
||||
done ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Loading…
Reference in a new issue