From the GPT review of R4 (row211-r4-gpt.md, N1 and N2), for #1709 and #426: - slot-busy.sh (N1): gone() lists the proc root with ls -f -1 --quoting-style=literal, so QUOTING_STYLE can't quote a pid out of the listing, and the script exports LC_ALL=C before any tool runs. A failed or denied listing is still never gone. - slot-busy.sh (N2): a command line holding quotes, backslashes, shell operators or uneven whitespace gets a flat form, made in awk. The snapshot rule reads the flat form. The general rules read the flat form, then the raw text. A wrapper's text goes through an awk word walk: every word of a source or . span counts, quoted text is read again, and the wrapper's own snapshot is skipped. More than 32 walk levels, an awk failure or bad output is exit 2, "cannot classify". - test-git-guard.sh: 48 new cases, 153 in all. The new cases cover QUOTING_STYLE, the locale, the rule shapes, depth, awk failure, and live wrappers for the review's 13 shapes plus a CLEAR control. - mutants.sh: sites R5 moved are updated, and 27 new mutants bring the total to 57, all killed. RED 119/34 on fd1aa8cc8's slot-busy.sh, GREEN 153/0. The hook and the installer are unchanged.
646 lines
37 KiB
Bash
Executable File
646 lines
37 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Sandboxed selftests for the push guard, its installer and the busy check
|
|
# (row 211, #426): scripts/git-hooks/pre-push, scripts/install-git-hooks.sh
|
|
# and scripts/slot-busy.sh.
|
|
#
|
|
# Pushes go from a scratch clone and its worktree to a scratch bare remote,
|
|
# never to origin. Git sees no user or system config. The busy check runs
|
|
# against a fake /proc (pids above pid_max) for the exit codes and rules, and
|
|
# against the real /proc with a sleeping stand-in whose argv is `node --test`.
|
|
set -uo pipefail
|
|
cd "$(dirname "$0")/.."
|
|
REPO="$(pwd)"
|
|
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
|
|
|
|
SANDBOX="$(cd "$(mktemp -d)" && pwd -P)"
|
|
STANDIN=""
|
|
KIDS="" # wrapper shells and their children that the suite starts
|
|
LOCKED="" # directories a test makes unreadable; cleanup gives modes back
|
|
cleanup() {
|
|
if [ -n "$STANDIN" ]; then kill "$STANDIN" 2>/dev/null; wait "$STANDIN" 2>/dev/null; fi
|
|
for k in $KIDS; do kill "$k" 2>/dev/null; wait "$k" 2>/dev/null; done
|
|
for l in $LOCKED; do chmod 0755 "$l" 2>/dev/null; done
|
|
rm -rf "$SANDBOX"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
# Status colors: terminal-only, NO_COLOR-respecting; plain when piped.
|
|
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
C_OK=$'\033[0;32m'; C_FAIL=$'\033[0;31m'; C_RESET=$'\033[0m'
|
|
else
|
|
C_OK=""; C_FAIL=""; C_RESET=""
|
|
fi
|
|
check() {
|
|
if [ "$2" = "0" ]; then PASS=$((PASS+1)); echo "${C_OK}OK${C_RESET} $1"; else FAIL=$((FAIL+1)); echo "${C_FAIL}FAIL${C_RESET} $1"; fi
|
|
}
|
|
has() { [[ "$1" == *"$2"* ]]; }
|
|
|
|
# Git sees no user or system config: no hooks path, no signing, a fixed identity.
|
|
: > "$SANDBOX/gitconfig"
|
|
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL="$SANDBOX/gitconfig"
|
|
export GIT_AUTHOR_NAME=suite GIT_AUTHOR_EMAIL=suite@local GIT_COMMITTER_NAME=suite GIT_COMMITTER_EMAIL=suite@local
|
|
|
|
HOOK_SRC="$REPO/scripts/git-hooks/pre-push"
|
|
INSTALL="$REPO/scripts/install-git-hooks.sh"
|
|
BUSY="$REPO/scripts/slot-busy.sh"
|
|
|
|
B="$SANDBOX/remote.git"
|
|
C="$SANDBOX/clone"
|
|
W="$SANDBOX/wt"
|
|
O="$SANDBOX/other"
|
|
commit() { # $1: repo, $2: file and message
|
|
echo "$2" > "$1/$2" && git -C "$1" add "$2" && git -C "$1" commit -q -m "$2"
|
|
}
|
|
rtip() { git -C "$B" rev-parse -q --verify "refs/heads/$1"; }
|
|
|
|
# ---- the remote, a clone with the guard, a worktree ----
|
|
git init -q --bare -b main "$B"
|
|
git init -q -b main "$SANDBOX/seed"
|
|
commit "$SANDBOX/seed" base
|
|
git -C "$SANDBOX/seed" push -q "$B" main
|
|
git clone -q "$B" "$C"
|
|
|
|
echo "== install =="
|
|
out=$(bash "$INSTALL" --repo "$C" 2>&1); rc=$?
|
|
check "the install into a clean clone exits 0" "$rc"
|
|
check "it reports the install and a passed canary" "$(has "$out" "ok installed the push guard" && has "$out" "canary passed"; echo $?)"
|
|
check "the hook is in the common .git/hooks, with the repository's bytes, executable" \
|
|
"$(cmp -s "$HOOK_SRC" "$C/.git/hooks/pre-push" && [ -x "$C/.git/hooks/pre-push" ]; echo $?)"
|
|
check "no temporary file is left in .git/hooks" \
|
|
"$(ls -A "$C/.git/hooks" | grep -q '^\.pre-push\.install'; [ $? = 1 ]; echo $?)"
|
|
git -C "$C" worktree add -q -b seat/w "$W" origin/main
|
|
check "the worktree's git runs the common hook" \
|
|
"$([ "$(git -C "$W" rev-parse --path-format=absolute --git-path hooks/pre-push)" = "$C/.git/hooks/pre-push" ]; echo $?)"
|
|
|
|
echo "== pushes from the worktree =="
|
|
commit "$W" one
|
|
out=$(git -C "$W" push origin seat/w 2>&1); rc=$?
|
|
check "a new branch passes" "$rc"
|
|
check "the remote branch is at the pushed commit" "$([ "$(rtip seat/w)" = "$(git -C "$W" rev-parse HEAD)" ]; echo $?)"
|
|
|
|
commit "$W" two
|
|
out=$(git -C "$W" push origin seat/w 2>&1); rc=$?
|
|
check "a fast-forward passes" "$rc"
|
|
check "the remote branch moved forward" "$([ "$(rtip seat/w)" = "$(git -C "$W" rev-parse HEAD)" ]; echo $?)"
|
|
|
|
before=$(rtip seat/w)
|
|
git -C "$W" reset -q --hard HEAD~1
|
|
commit "$W" two-rewritten
|
|
after=$(git -C "$W" rev-parse HEAD)
|
|
out=$(git -C "$W" push --force origin seat/w 2>&1); rc=$?
|
|
check "push --force of a rewritten branch is refused" "$([ "$rc" -ne 0 ]; echo $?)"
|
|
check "the refusal is the guard's, and names the ref and both shas" \
|
|
"$(has "$out" "mosaic push guard: refused: refs/heads/seat/w: not a fast-forward" && has "$out" "$before" && has "$out" "$after"; echo $?)"
|
|
check "the remote ref is unchanged after the refused --force" "$([ "$(rtip seat/w)" = "$before" ]; echo $?)"
|
|
|
|
out=$(git -C "$W" push origin +seat/w:seat/w 2>&1); rc=$?
|
|
check "a +refspec push is refused" "$([ "$rc" -ne 0 ] && has "$out" "not a fast-forward"; echo $?)"
|
|
check "the remote ref is unchanged after the refused +refspec" "$([ "$(rtip seat/w)" = "$before" ]; echo $?)"
|
|
|
|
out=$(git -C "$W" push --force-with-lease origin seat/w 2>&1); rc=$?
|
|
check "push --force-with-lease is refused too" "$([ "$rc" -ne 0 ] && has "$out" "not a fast-forward"; echo $?)"
|
|
|
|
out=$(git -C "$W" push origin :seat/w 2>&1); rc=$?
|
|
check "a delete (:ref) is refused" "$([ "$rc" -ne 0 ] && has "$out" "mosaic push guard: refused: refs/heads/seat/w: a delete"; echo $?)"
|
|
out=$(git -C "$W" push --delete origin seat/w 2>&1); rc=$?
|
|
check "a delete (--delete) is refused" "$([ "$rc" -ne 0 ] && has "$out" "a delete"; echo $?)"
|
|
check "the remote branch still exists after the refused deletes" "$([ "$(rtip seat/w)" = "$before" ]; echo $?)"
|
|
|
|
# Another clone moves the remote; this clone never fetches it.
|
|
git clone -q -b seat/w "$B" "$O"
|
|
commit "$O" from-other
|
|
git -C "$O" push -q origin seat/w
|
|
moved=$(rtip seat/w)
|
|
check "the other clone moved the remote, and this clone lacks that commit" \
|
|
"$([ "$moved" != "$before" ] && ! git -C "$C" cat-file -e "$moved^{commit}" 2>/dev/null; echo $?)"
|
|
out=$(git -C "$W" push --force origin seat/w 2>&1); rc=$?
|
|
check "a push over a remote tip missing locally is refused, saying fetch first" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "isn't in this repository; fetch first"; echo $?)"
|
|
check "the remote ref is unchanged after that refusal" "$([ "$(rtip seat/w)" = "$moved" ]; echo $?)"
|
|
check "the guard fetched nothing" "$(! git -C "$C" cat-file -e "$moved^{commit}" 2>/dev/null; echo $?)"
|
|
|
|
echo "== tags =="
|
|
# An existing tag never changes. Both cases below pass the ancestor check: a
|
|
# new tag object on the same commit peels to that commit, and a moved
|
|
# lightweight tag's old commit is an ancestor of its new one.
|
|
tagtip() { git -C "$B" rev-parse -q --verify "refs/tags/$1"; }
|
|
git -C "$W" tag -a -m first t-ann HEAD
|
|
out=$(git -C "$W" push origin t-ann 2>&1); rc=$?
|
|
check "a new annotated tag passes" "$([ "$rc" = 0 ] && [ "$(tagtip t-ann)" = "$(git -C "$W" rev-parse t-ann)" ]; echo $?)"
|
|
first=$(tagtip t-ann)
|
|
git -C "$W" tag -f -a -m second t-ann HEAD >/dev/null
|
|
out=$(git -C "$W" push --force origin t-ann 2>&1); rc=$?
|
|
check "push --force of a new tag object on the same commit is refused" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "mosaic push guard: refused: refs/tags/t-ann: an existing tag" && has "$out" "$first"; echo $?)"
|
|
check "the remote tag is still the first tag object" "$([ "$(tagtip t-ann)" = "$first" ]; echo $?)"
|
|
git -C "$W" tag t-light HEAD~1
|
|
out=$(git -C "$W" push origin t-light 2>&1); rc=$?
|
|
check "a new lightweight tag passes" "$rc"
|
|
git -C "$W" tag -f t-light HEAD >/dev/null
|
|
out=$(git -C "$W" push --force origin t-light 2>&1); rc=$?
|
|
check "push --force of a lightweight tag moved to a descendant is refused" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "mosaic push guard: refused: refs/tags/t-light: an existing tag"; echo $?)"
|
|
check "the remote lightweight tag hasn't moved" "$([ "$(tagtip t-light)" = "$(git -C "$W" rev-parse HEAD~1)" ]; echo $?)"
|
|
out=$(git -C "$W" push origin :refs/tags/t-light 2>&1); rc=$?
|
|
check "a tag delete is refused, and the tag stays" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "refs/tags/t-light: a delete" && [ -n "$(tagtip t-light)" ]; echo $?)"
|
|
|
|
echo "== the hook, fed by hand =="
|
|
z=0000000000000000000000000000000000000000
|
|
hookrun() { (cd "$C" && printf '%s\n' "$1" | sh "$C/.git/hooks/pre-push" origin "$B" 2>&1); }
|
|
out=$(hookrun "refs/heads/x $(git -C "$C" rev-parse HEAD) refs/heads/x"); rc=$?
|
|
check "a short ref line is refused" "$([ "$rc" -ne 0 ] && has "$out" "unreadable ref line"; echo $?)"
|
|
out=$(hookrun "refs/heads/x HEAD refs/heads/x $z"); rc=$?
|
|
check "a non-hex object name is refused" "$([ "$rc" -ne 0 ] && has "$out" "non-hex"; echo $?)"
|
|
out=$(hookrun "refs/heads/x $(git -C "$C" rev-parse HEAD) refs/heads/x 0"); rc=$?
|
|
check "a remote object name of 0 is refused, not read as a new ref" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "isn't 40 or 64 hex characters"; echo $?)"
|
|
out=$(hookrun "refs/heads/x $(git -C "$C" rev-parse HEAD | cut -c1-39) refs/heads/x $z"); rc=$?
|
|
check "a 39-character local object name is refused" "$([ "$rc" -ne 0 ] && has "$out" "isn't 40 or 64 hex characters"; echo $?)"
|
|
out=$(hookrun "refs/heads/x ${z}${z:0:24} refs/heads/x $(git -C "$C" rev-parse HEAD)"); rc=$?
|
|
check "a 64-character object name passes the length check (64 zeros refuse as a delete)" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "refs/heads/x: a delete" && ! has "$out" "isn't 40 or 64"; echo $?)"
|
|
out=$(hookrun "refs/heads/x 1234567890123456789012345678901234567890 refs/heads/x $(git -C "$C" rev-parse HEAD)"); rc=$?
|
|
check "a failed ancestor check (an unknown local object) is refused" \
|
|
"$([ "$rc" -ne 0 ] && has "$out" "the ancestor check failed"; echo $?)"
|
|
out=$(printf '' | (cd "$C" && sh "$C/.git/hooks/pre-push" origin "$B" 2>&1)); rc=$?
|
|
check "an empty push passes" "$rc"
|
|
|
|
echo "== installer refusals =="
|
|
fresh() { rm -rf "$1"; git clone -q "$B" "$1"; }
|
|
fresh "$SANDBOX/hp"
|
|
git -C "$SANDBOX/hp" config core.hooksPath .husky
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/hp" 2>&1); rc=$?
|
|
check "the install refuses with core.hooksPath set (exit 2)" "$([ "$rc" = 2 ]; echo $?)"
|
|
check "it says core.hooksPath is set" "$(has "$out" "core.hooksPath is set"; echo $?)"
|
|
check "no hook was written under the hooksPath refusal" \
|
|
"$([ ! -e "$SANDBOX/hp/.git/hooks/pre-push" ] && [ ! -e "$SANDBOX/hp/.husky/pre-push" ]; echo $?)"
|
|
|
|
fresh "$SANDBOX/df"
|
|
printf '#!/bin/sh\nexit 0\n' > "$SANDBOX/df/.git/hooks/pre-push"
|
|
chmod 0755 "$SANDBOX/df/.git/hooks/pre-push"
|
|
ln "$SANDBOX/df/.git/hooks/pre-push" "$SANDBOX/df-old"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/df" 2>&1); rc=$?
|
|
check "the install refuses a different installed pre-push (exit 2)" \
|
|
"$([ "$rc" = 2 ] && has "$out" "a different pre-push hook is installed"; echo $?)"
|
|
check "the different hook is untouched" \
|
|
"$([ "$(cat "$SANDBOX/df/.git/hooks/pre-push")" = "$(printf '#!/bin/sh\nexit 0')" ]; echo $?)"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/df" --force-install 2>&1); rc=$?
|
|
check "--force-install replaces it" \
|
|
"$([ "$rc" = 0 ] && has "$out" "replaced a different hook" && cmp -s "$HOOK_SRC" "$SANDBOX/df/.git/hooks/pre-push"; echo $?)"
|
|
check "the replacement was a rename: the old file keeps its bytes" \
|
|
"$([ "$(cat "$SANDBOX/df-old")" = "$(printf '#!/bin/sh\nexit 0')" ]; echo $?)"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/df" 2>&1); rc=$?
|
|
check "a second install over the same bytes exits 0" "$([ "$rc" = 0 ] && has "$out" "the same bytes"; echo $?)"
|
|
|
|
fresh "$SANDBOX/ln"
|
|
ln -s /dev/null "$SANDBOX/ln/.git/hooks/pre-push"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/ln" --force-install 2>&1); rc=$?
|
|
check "the install refuses a symlinked pre-push, even with --force-install" "$([ "$rc" = 2 ] && has "$out" "is a symlink"; echo $?)"
|
|
|
|
# A hooks directory that links to a shared one would put the hook outside
|
|
# the repository, in every repository that shares it.
|
|
fresh "$SANDBOX/sl"
|
|
mkdir -p "$SANDBOX/shared-hooks"
|
|
rm -rf "$SANDBOX/sl/.git/hooks"
|
|
ln -s "$SANDBOX/shared-hooks" "$SANDBOX/sl/.git/hooks"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/sl" 2>&1); rc=$?
|
|
check "the install refuses a symlinked hooks directory (exit 2)" "$([ "$rc" = 2 ] && has "$out" "/.git/hooks is a symlink"; echo $?)"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/sl" --force-install 2>&1); rc=$?
|
|
check "it refuses it with --force-install too" "$([ "$rc" = 2 ] && has "$out" "/.git/hooks is a symlink"; echo $?)"
|
|
check "nothing was written in the directory the symlink names" "$([ -z "$(ls -A "$SANDBOX/shared-hooks")" ]; echo $?)"
|
|
|
|
# The temporary copy's name isn't predictable from the installer's pid. The
|
|
# subshell plants a link at the pid-named path, then execs the installer, so
|
|
# the installer's $$ is that pid.
|
|
fresh "$SANDBOX/pt"
|
|
printf 'victim\n' > "$SANDBOX/pt-victim"
|
|
out=$(pid=$BASHPID; ln -s "$SANDBOX/pt-victim" "$SANDBOX/pt/.git/hooks/.pre-push.install-git-hooks.$pid" && exec bash "$INSTALL" --repo "$SANDBOX/pt" 2>&1); rc=$?
|
|
check "the install writes nothing through a link planted at a pid-named temporary path" \
|
|
"$([ "$rc" = 0 ] && [ "$(cat "$SANDBOX/pt-victim")" = victim ]; echo $?)"
|
|
check "the installed hook is a regular file with the repository's bytes" \
|
|
"$([ -f "$SANDBOX/pt/.git/hooks/pre-push" ] && [ ! -L "$SANDBOX/pt/.git/hooks/pre-push" ] && cmp -s "$HOOK_SRC" "$SANDBOX/pt/.git/hooks/pre-push"; echo $?)"
|
|
|
|
fresh "$SANDBOX/wd"
|
|
git -C "$SANDBOX/wd" worktree add -q -b seat/x "$SANDBOX/wd-wt" origin/main
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/wd-wt" 2>&1); rc=$?
|
|
check "an install named by a worktree path lands in the common .git/hooks" \
|
|
"$([ "$rc" = 0 ] && cmp -s "$HOOK_SRC" "$SANDBOX/wd/.git/hooks/pre-push"; echo $?)"
|
|
|
|
out=$(GIT_DIR="$SANDBOX/wd/.git" bash "$INSTALL" --repo "$SANDBOX/wd" 2>&1); rc=$?
|
|
check "the install refuses with GIT_DIR set" "$([ "$rc" = 2 ] && has "$out" "GIT_DIR is set"; echo $?)"
|
|
mkdir -p "$SANDBOX/plain"
|
|
out=$(bash "$INSTALL" --repo "$SANDBOX/plain" 2>&1); rc=$?
|
|
check "the install refuses a directory that isn't a checkout" "$([ "$rc" = 2 ]; echo $?)"
|
|
out=$(bash "$INSTALL" --nope 2>&1); rc=$?
|
|
check "an unknown argument exits 4" "$([ "$rc" = 4 ]; echo $?)"
|
|
|
|
echo "== slot-busy, fake /proc =="
|
|
P="$SANDBOX/proc"
|
|
fakeproc() { # $1: pid, $2: cwd, rest: argv
|
|
local pid=$1 cwd=$2; shift 2
|
|
mkdir -p "$P/$pid"
|
|
printf '%s\0' "$@" > "$P/$pid/cmdline"
|
|
ln -sfn "$cwd" "$P/$pid/cwd"
|
|
}
|
|
busyrun() { bash "$BUSY" --proc-root "$P" 2>&1; }
|
|
fakeproc 4194401 /srv node server.mjs
|
|
fakeproc 4194402 /srv grep -r gate .
|
|
# Both wrappers' text would match a rule without the snapshot exclusion.
|
|
fakeproc 4194403 /srv /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && node --test x.test.mjs"
|
|
fakeproc 4194405 /srv /usr/bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-2-ab.sh 2>/dev/null || true && eval 'bash scripts/test-x.sh > x.log' && pwd -P >| /home/u/cwd"
|
|
# A wrapper that sources files that aren't gates, tests or mutants, then
|
|
# runs a gate as a child: the child is matched, the wrapper isn't.
|
|
fakeproc 4194406 /srv /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh && source .venv/bin/activate && . ./env.sh && ./gate.sh 253"
|
|
mkdir -p "$P/4194404"; : > "$P/4194404/cmdline"
|
|
out=$(busyrun); rc=$?
|
|
check "no matching process: exit 0 and CLEAR" "$([ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
|
check "an agent's shell-snapshot wrapper isn't a match" "$(! has "$out" "4194403" && ! has "$out" "4194405"; echo $?)"
|
|
check "a wrapper that sources other files and runs a gate as a child isn't a match" "$(! has "$out" "4194406"; echo $?)"
|
|
|
|
fakeproc 4194410 /seat/a node --test packages/x/tests/a.test.mjs
|
|
out=$(busyrun); rc=$?
|
|
check "a node --test process: exit 1" "$([ "$rc" = 1 ]; echo $?)"
|
|
check "its BUSY line names pid, rule and cwd" "$(has "$out" "BUSY 4194410 node --test cwd=/seat/a node --test"; echo $?)"
|
|
check "BUSY output never says CLEAR" "$(! has "$out" "CLEAR"; echo $?)"
|
|
ran=$(bash "$BUSY" --proc-root "$P" >/dev/null && echo ran)
|
|
check "\`slot-busy > LOG && step\` doesn't run the step while BUSY" "$([ -z "$ran" ]; echo $?)"
|
|
rm -rf "${P:?}/4194410"
|
|
|
|
rule() { # $1: expected rule, rest: argv
|
|
local want=$1; shift
|
|
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b "$@"
|
|
out=$(busyrun); rc=$?
|
|
check "busy on: ${*//$'\n'/\\n} ($want)" "$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 $want "; echo $?)"
|
|
}
|
|
rule "node --test" /usr/bin/node --experimental-strip-types --test a.test.ts
|
|
rule "gate" bash /mnt/scratch/sage-gate.sh 253
|
|
rule "gate" bash ./gate.sh
|
|
rule "mutant" bash agents/x/work/queue-1/mutants.sh M3
|
|
rule "test script" bash scripts/test-land-work.sh
|
|
rule "docker compose" docker compose -p x up -d
|
|
rule "docker compose" /usr/bin/docker-compose up
|
|
# The snapshot exclusion is for the wrapper's shape only: the same command
|
|
# without the snapshot source, and real runs whose arguments hold the path.
|
|
rule "node --test" /bin/bash -c "node --test x.test.mjs"
|
|
rule "node --test" node --test /seat/tests/shell-snapshots/snapshot-guard.test.mjs
|
|
rule "gate" bash /seat/shell-snapshots/snapshot-gate.sh
|
|
# A wrapper that sources a gate, test or mutant script runs it in its own
|
|
# shell, with no child to match, so the wrapper itself is BUSY.
|
|
SNAP="source /home/u/.claude/shell-snapshots/snapshot-bash-1.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source /seat/scripts/test-x.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && . /seat/scripts/test-x.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source \"scripts/test-x.sh\" arg"
|
|
# A sourced file that isn't one comes first; the one after it still counts.
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source .venv/bin/activate && . scripts/test-x.sh"
|
|
rule "sourced gate" /usr/bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-2-ab.sh 2>/dev/null || true && eval '. ./gate.sh 253' && pwd -P >| /home/u/cwd"
|
|
rule "sourced mutant" /bin/bash -c "$SNAP && cd /seat && eval 'source agents/x/work/queue-1/mutants.sh M3'"
|
|
# R5 (N2): every word up to the next operator counts, not only the first; and
|
|
# quoted, escaped and nested forms read the way the shell reads them.
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source 2>/dev/null /seat/scripts/test-x.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source \$(cd scripts && pwd)/test-x.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source \`cd scripts; pwd\`/test-x.sh"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && source \"/seat/a;b (c)/scripts/test-x.sh\""
|
|
rule "sourced test script" /bin/bash -c "$SNAP && eval 'source \"/seat/a;b/scripts/test-x.sh\"'"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && eval \"source \\\"/seat/dir with spaces/scripts/test-x.sh\\\"\""
|
|
rule "sourced test script" /usr/bin/bash -c "$SNAP 2>/dev/null || true && eval 'source '\\''scripts/test-x.sh'\\''' && pwd -P >| /home/u/cwd"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && eval \$'source scripts/test-x.sh'"
|
|
rule "sourced test script" /bin/bash -c "$SNAP && cat <<EOF"$'\n'"don't"$'\n'"EOF"$'\n'"source \"it's dir/scripts/test-x.sh\""
|
|
# An unclosed quote runs to the end of the text and is read again there.
|
|
rule "sourced test script" /bin/bash -c "$SNAP && echo \"it's && source /seat/scripts/test-x.sh"
|
|
# The general rules read the flat form too, and still read the raw one.
|
|
rule "test script" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
|
rule "gate" bash "/seat/gate;1.sh"
|
|
# Two blanks in a row make one in the flat form, with no other character to
|
|
# flatten.
|
|
rule "docker compose" /bin/sh -c "docker compose -p x up"
|
|
rule "docker compose" /bin/sh -c $'docker\tcompose -p x up'
|
|
rm -rf "${P:?}/4194420"
|
|
idle() { # argv: a process that must leave the check CLEAR
|
|
rm -rf "${P:?}/4194421"; fakeproc 4194421 /seat/c "$@"
|
|
out=$(busyrun); rc=$?
|
|
check "not busy: ${*//$'\n'/\\n}" "$([ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
|
rm -rf "${P:?}/4194421"
|
|
}
|
|
# The harness's own snapshot path is skipped, quoted or not, whatever its name.
|
|
idle /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && echo hi"
|
|
idle /bin/bash -c "source \"/home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh\" && echo hi"
|
|
idle /bin/bash -c "source /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && . /home/u/.claude/shell-snapshots/snapshot-bash-3-gate.sh && echo hi"
|
|
# Locale: a byte that isn't UTF-8 must not end a name under a UTF-8 locale.
|
|
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b bash $'scripts/test-\xffx.sh'
|
|
out=$(LC_ALL=C.UTF-8 bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "under LC_ALL=C.UTF-8, a test script name holding the byte 0xff: exit 1" \
|
|
"$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 test script "; echo $?)"
|
|
# Quoting that nests deeper than the walk reads is an error, never CLEAR. Each
|
|
# level escapes backslashes and double quotes and wraps the text in quotes.
|
|
nest() { # $1: levels. Sets w.
|
|
w='source scripts/test-x.sh'
|
|
for _ in $(seq "$1"); do w=$(printf '%s' "$w" | sed 's/[\\"]/\\&/g'); w="\"$w\""; done
|
|
}
|
|
nest 16
|
|
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b /bin/bash -c "$SNAP && eval $w"
|
|
out=$(busyrun); rc=$?
|
|
check "a sourced test under 16 escaped quoting levels (${#w} bytes): exit 1, sourced test script" \
|
|
"$([ "$rc" = 1 ] && has "$out" "BUSY 4194420 sourced test script "; echo $?)"
|
|
nest 17
|
|
rm -rf "${P:?}/4194420"; fakeproc 4194420 /seat/b /bin/bash -c "$SNAP && eval $w"
|
|
out=$(busyrun); rc=$?
|
|
check "quoting 17 escaped levels deep (${#w} bytes): exit 2, cannot classify, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "cannot classify the command line of $P/4194420" && ! has "$out" CLEAR; echo $?)"
|
|
rm -rf "${P:?}/4194420"
|
|
|
|
echo "== slot-busy, read errors =="
|
|
# A pid directory that stays while its cmdline can't be read is an error,
|
|
# never a vanished process: exit 2 and no CLEAR.
|
|
mkdir -p "$P/4194430"
|
|
out=$(busyrun); rc=$?
|
|
check "a pid directory with no cmdline: exit 2" "$([ "$rc" = 2 ]; echo $?)"
|
|
check "it names that cmdline and never says CLEAR" "$(has "$out" "cannot read $P/4194430/cmdline" && ! has "$out" CLEAR; echo $?)"
|
|
rm -rf "${P:?}/4194430"
|
|
mkdir -p "$P/4194431/cmdline"
|
|
out=$(busyrun); rc=$?
|
|
check "a cmdline that can't be read (a directory): exit 2, no CLEAR" "$([ "$rc" = 2 ] && ! has "$out" CLEAR; echo $?)"
|
|
fakeproc 4194410 /seat/a node --test a.test.mjs
|
|
out=$(busyrun); rc=$?
|
|
check "a read error beside a BUSY process: exit 2, the BUSY line kept, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "BUSY 4194410 node --test" && ! has "$out" CLEAR; echo $?)"
|
|
rm -rf "${P:?}/4194431"
|
|
chmod 000 "$P/4194410/cmdline"
|
|
out=$(busyrun); rc=$?
|
|
check "a node --test process whose cmdline is unreadable (mode 000) is never CLEAR" "$([ "$rc" != 0 ] && ! has "$out" CLEAR; echo $?)"
|
|
rm -rf "${P:?}/4194410"
|
|
|
|
mkdir -p "$SANDBOX/failtr" "$SANDBOX/vanishtr" "$SANDBOX/failawk"
|
|
printf '#!/bin/sh\nexit 1\n' > "$SANDBOX/failtr/tr"
|
|
printf '#!/bin/sh\nexit 1\n' > "$SANDBOX/failawk/awk"
|
|
# This tr stands in for a process that exits during the read: for pid
|
|
# 4194440 it removes the pid directory and fails; otherwise it is tr.
|
|
cat > "$SANDBOX/vanishtr/tr" <<EOF
|
|
#!/bin/sh
|
|
f=\$(readlink "/proc/\$\$/fd/0")
|
|
case "\$f" in */4194440/cmdline) rm -rf "\${f%/cmdline}"; exit 1 ;; esac
|
|
exec $(command -v tr) "\$@"
|
|
EOF
|
|
chmod +x "$SANDBOX/failtr/tr" "$SANDBOX/failawk/awk" "$SANDBOX/vanishtr/tr"
|
|
out=$(PATH="$SANDBOX/failtr:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "a failing read (tr exits 1) with every pid directory still there: exit 2, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "cannot read" && ! has "$out" CLEAR; echo $?)"
|
|
fakeproc 4194440 /seat/v node --test v.test.mjs
|
|
out=$(PATH="$SANDBOX/vanishtr:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "a process that vanishes during its read isn't an error: exit 0 and CLEAR" \
|
|
"$([ "$rc" = 0 ] && [ "$out" = CLEAR ] && [ ! -e "$P/4194440" ]; echo $?)"
|
|
rm -rf "${P:?}/4194440"
|
|
out=$(PATH="$SANDBOX/failawk:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "its own process unreadable (awk exits 1): exit 2, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "cannot read its own process" && ! has "$out" CLEAR; echo $?)"
|
|
|
|
# Every ancestor's status read counts, not only the first. The awk below is
|
|
# the real awk for call 1 (the scan's own status) and then fails or prints
|
|
# nothing; a third one prints a pid that isn't in /proc on call 2, so the
|
|
# walk meets an ancestor that has exited.
|
|
mkdir -p "$SANDBOX/awkn"
|
|
cat > "$SANDBOX/awkn/awk" <<EOF
|
|
#!/bin/sh
|
|
n=\$(cat "$SANDBOX/awkn/n" 2>/dev/null || echo 0); n=\$((n + 1)); echo "\$n" > "$SANDBOX/awkn/n"
|
|
if [ "\$n" -ge 2 ]; then
|
|
case \$(cat "$SANDBOX/awkn/mode") in
|
|
fail) exit 1 ;;
|
|
empty) exit 0 ;;
|
|
exited) if [ "\$n" = 2 ]; then echo 4194999; exit 0; fi ;;
|
|
esac
|
|
fi
|
|
exec $(command -v awk) "\$@"
|
|
EOF
|
|
chmod +x "$SANDBOX/awkn/awk"
|
|
awkn() { # $1: fail, empty or exited; rest: NAME=VALUE for the check's environment.
|
|
echo "$1" > "$SANDBOX/awkn/mode"; rm -f "$SANDBOX/awkn/n"; shift
|
|
out=$(env "$@" PATH="$SANDBOX/awkn:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
calls=$(cat "$SANDBOX/awkn/n" 2>/dev/null || echo 0)
|
|
}
|
|
awkn fail
|
|
check "an ancestor's status read that fails after its own read passed: exit 2, no CLEAR" \
|
|
"$([ "$calls" -ge 2 ] && [ "$rc" = 2 ] && has "$out" "cannot read the status of ancestor" && ! has "$out" CLEAR; echo $?)"
|
|
awkn empty
|
|
check "an ancestor's status with no PPid: exit 2, no CLEAR" \
|
|
"$([ "$calls" -ge 2 ] && [ "$rc" = 2 ] && has "$out" "cannot read the status of ancestor" && ! has "$out" CLEAR; echo $?)"
|
|
awkn exited
|
|
check "an ancestor that has exited (its pid gone from /proc) ends the walk: exit 0 and CLEAR" \
|
|
"$([ "$calls" -ge 3 ] && [ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
|
|
|
# R5 (N1): the gone check reads the proc root's listing whatever the caller's
|
|
# QUOTING_STYLE is. A quoted name would read as a vanished pid, and both
|
|
# failed reads above would turn into CLEAR.
|
|
for qs in shell-always c escape shell-escape-always locale; do
|
|
out=$(QUOTING_STYLE=$qs PATH="$SANDBOX/failtr:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "QUOTING_STYLE=$qs: a failing cmdline read with the pid still listed: exit 2, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "cannot read $P/" && ! has "$out" CLEAR; echo $?)"
|
|
awkn fail QUOTING_STYLE=$qs
|
|
check "QUOTING_STYLE=$qs: a failing ancestor status read with the pid still listed: exit 2, no CLEAR" \
|
|
"$([ "$calls" -ge 2 ] && [ "$rc" = 2 ] && has "$out" "cannot read the status of ancestor" && ! has "$out" CLEAR; echo $?)"
|
|
done
|
|
|
|
# The flat form and the walk over a wrapper's text are awk programs. If
|
|
# either awk fails, or prints nothing, the command line can't be classified:
|
|
# exit 2, never CLEAR. This awk fails or goes silent for one program only
|
|
# (the walk has `function walk`, the flat form `gsub`).
|
|
mkdir -p "$SANDBOX/lexawk"
|
|
cat > "$SANDBOX/lexawk/awk" <<EOF
|
|
#!/bin/sh
|
|
m=\$(cat "$SANDBOX/lexawk/mode")
|
|
case "\$*" in
|
|
*"function walk"*) case \$m in walk-fail) cat > /dev/null; exit 1 ;; walk-empty) cat > /dev/null; exit 0 ;; esac ;;
|
|
*gsub*) case \$m in flat-fail) cat > /dev/null; exit 1 ;; flat-empty) cat > /dev/null; exit 0 ;; esac ;;
|
|
esac
|
|
exec $(command -v awk) "\$@"
|
|
EOF
|
|
chmod +x "$SANDBOX/lexawk/awk"
|
|
lexawk() { # $1: mode, $2: label, rest: argv of a process that is BUSY
|
|
local mode=$1 label=$2; shift 2
|
|
fakeproc 4194460 /seat/w "$@"
|
|
echo "$mode" > "$SANDBOX/lexawk/mode"
|
|
out=$(PATH="$SANDBOX/lexawk:$PATH" bash "$BUSY" --proc-root "$P" 2>&1); rc=$?
|
|
check "$label: exit 2, cannot classify, no CLEAR" \
|
|
"$([ "$rc" = 2 ] && has "$out" "cannot classify the command line of $P/4194460" && ! has "$out" CLEAR; echo $?)"
|
|
rm -rf "${P:?}/4194460"
|
|
}
|
|
lexawk walk-fail "the wrapper walk's awk exits 1" /bin/bash -c "$SNAP && source /seat/scripts/test-x.sh"
|
|
lexawk walk-empty "the wrapper walk's awk prints nothing" /bin/bash -c "$SNAP && source /seat/scripts/test-x.sh"
|
|
# Only the flat form names this one (`.sh;` ends no rule's match as it is).
|
|
lexawk flat-fail "the flat form's awk exits 1" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
|
lexawk flat-empty "the flat form's awk prints nothing" /bin/bash -c "bash scripts/test-x.sh;echo done"
|
|
|
|
# A lookup that is denied isn't a process that vanished. This tr makes the
|
|
# proc root unreadable once the pid directory is listed, then fails the read;
|
|
# the pid directory and its node --test cmdline stay where they are.
|
|
DP="$SANDBOX/dproc"
|
|
mkdir -p "$SANDBOX/denytr" "$DP/4194450"
|
|
printf '%s\0' node --test d.test.mjs > "$DP/4194450/cmdline"
|
|
cat > "$SANDBOX/denytr/tr" <<EOF
|
|
#!/bin/sh
|
|
f=\$(readlink "/proc/\$\$/fd/0")
|
|
case "\$f" in */4194450/cmdline) chmod 000 "$DP"; exit 1 ;; esac
|
|
exec $(command -v tr) "\$@"
|
|
EOF
|
|
chmod +x "$SANDBOX/denytr/tr"
|
|
LOCKED="$LOCKED $DP"
|
|
out=$(PATH="$SANDBOX/denytr:$PATH" bash "$BUSY" --proc-root "$DP" 2>&1); rc=$?
|
|
denied=$([ ! -r "$DP" ] && echo yes); chmod 0755 "$DP"
|
|
check "a lookup denied after the listing (proc root mode 000): exit 2, no CLEAR" \
|
|
"$([ "$denied" = yes ] && [ "$rc" = 2 ] && has "$out" "cannot read $DP/4194450/cmdline" && ! has "$out" CLEAR; echo $?)"
|
|
check "the denied pid directory and its node --test cmdline were still there" \
|
|
"$([ "$(tr '\0' ' ' < "$DP/4194450/cmdline")" = "node --test d.test.mjs " ]; echo $?)"
|
|
# The root turns unreadable after the first pid's read went through: the next
|
|
# pid, a node --test, is a doubt, not a skip.
|
|
rm -rf "${DP:?}/4194450"
|
|
mkdir -p "$SANDBOX/locktr" "$DP/4194451" "$DP/4194452"
|
|
printf '%s\0' sleep 60 > "$DP/4194451/cmdline"
|
|
printf '%s\0' node --test l.test.mjs > "$DP/4194452/cmdline"
|
|
cat > "$SANDBOX/locktr/tr" <<EOF
|
|
#!/bin/sh
|
|
f=\$(readlink "/proc/\$\$/fd/0")
|
|
case "\$f" in */4194451/cmdline) chmod 000 "$DP" ;; esac
|
|
exec $(command -v tr) "\$@"
|
|
EOF
|
|
chmod +x "$SANDBOX/locktr/tr"
|
|
out=$(PATH="$SANDBOX/locktr:$PATH" bash "$BUSY" --proc-root "$DP" 2>&1); rc=$?
|
|
denied=$([ ! -r "$DP" ] && echo yes); chmod 0755 "$DP"
|
|
check "a proc root that turns unreadable after one read hides no later process: exit 2, no CLEAR" \
|
|
"$([ "$denied" = yes ] && [ "$rc" = 2 ] && has "$out" "cannot read $DP/4194452/cmdline" && ! has "$out" CLEAR; echo $?)"
|
|
|
|
out=$(bash "$BUSY" --proc-root "$SANDBOX/no-such-proc" 2>&1); rc=$?
|
|
check "a missing proc root: exit 2" "$([ "$rc" = 2 ]; echo $?)"
|
|
mkdir -p "$SANDBOX/empty-proc"
|
|
out=$(bash "$BUSY" --proc-root "$SANDBOX/empty-proc" 2>&1); rc=$?
|
|
check "a proc root with no processes: exit 2" "$([ "$rc" = 2 ]; echo $?)"
|
|
out=$(bash "$BUSY" --nope 2>&1); rc=$?
|
|
check "an unknown argument: exit 2" "$([ "$rc" = 2 ]; echo $?)"
|
|
|
|
echo "== slot-busy, real /proc =="
|
|
mkdir -p "$SANDBOX/bin" "$SANDBOX/standin"
|
|
cat > "$SANDBOX/bin/node" <<'EOF'
|
|
#!/bin/sh
|
|
trap 'exit 0' TERM
|
|
while :; do sleep 0.2; done
|
|
EOF
|
|
chmod +x "$SANDBOX/bin/node"
|
|
(cd "$SANDBOX/standin" && exec "$SANDBOX/bin/node" --test standin.test.mjs) &
|
|
STANDIN=$!
|
|
for _ in $(seq 50); do
|
|
has "$(tr '\0' ' ' < "/proc/$STANDIN/cmdline" 2>/dev/null)" "--test standin" && break
|
|
sleep 0.1
|
|
done
|
|
out=$(bash "$BUSY" 2>&1); rc=$?
|
|
check "with a stand-in node --test running: exit 1" "$([ "$rc" = 1 ]; echo $?)"
|
|
check "the stand-in has a BUSY line with its cwd" \
|
|
"$(printf '%s\n' "$out" | grep -q "^BUSY $STANDIN node --test cwd=$SANDBOX/standin "; echo $?)"
|
|
check "this suite, an ancestor, has no BUSY line" "$(! printf '%s\n' "$out" | grep -q "^BUSY $$ "; echo $?)"
|
|
ran=$(bash "$BUSY" >/dev/null && echo ran)
|
|
check "\`slot-busy > LOG && step\` doesn't run the step with the stand-in up" "$([ -z "$ran" ]; echo $?)"
|
|
kill "$STANDIN" 2>/dev/null; wait "$STANDIN" 2>/dev/null
|
|
gone=$STANDIN; STANDIN=""
|
|
out=$(bash "$BUSY" 2>&1); rc=$?
|
|
check "after the stand-in exits, it has no BUSY line" "$(! printf '%s\n' "$out" | grep -q "^BUSY $gone "; echo $?)"
|
|
check "the real-/proc run exits 0 or 1, never 2" "$([ "$rc" = 0 ] || [ "$rc" = 1 ]; echo $?)"
|
|
|
|
echo "== slot-busy, real snapshot wrappers =="
|
|
# Live wrappers, each shown to the check through a proc root that links only
|
|
# its own real /proc directories, so other jobs on the host don't count.
|
|
mkdir -p "$SANDBOX/shell-snapshots" "$SANDBOX/scripts" "$SANDBOX/wrap"
|
|
: > "$SANDBOX/shell-snapshots/snapshot-x.sh"
|
|
mkfifo "$SANDBOX/wrap/fifo"
|
|
# Sourced: it says it's ready, then blocks in the read builtin, no child.
|
|
cat > "$SANDBOX/scripts/test-x.sh" <<EOF
|
|
exec 3<>"$SANDBOX/wrap/fifo"
|
|
: > "$SANDBOX/wrap/ready"
|
|
read -t 60 -u 3 _
|
|
EOF
|
|
(cd "$SANDBOX/wrap" && exec bash -c "source $SANDBOX/shell-snapshots/snapshot-x.sh && source $SANDBOX/scripts/test-x.sh") &
|
|
WRAP=$!; KIDS="$KIDS $WRAP"
|
|
for _ in $(seq 50); do [ -e "$SANDBOX/wrap/ready" ] && break; sleep 0.1; done
|
|
kids=$(cat "/proc/$WRAP/task/$WRAP/children") && kids="none$kids"
|
|
LP="$SANDBOX/lproc1"; mkdir -p "$LP"; ln -s "/proc/$WRAP" "$LP/$WRAP"
|
|
out=$(bash "$BUSY" --proc-root "$LP" 2>&1); rc=$?
|
|
check "a live wrapper that sourced a test script, blocked in a builtin with no child: exit 1" \
|
|
"$([ -e "$SANDBOX/wrap/ready" ] && [ "$kids" = none ] && [ "$rc" = 1 ] && ! has "$out" CLEAR; echo $?)"
|
|
check "its BUSY line names the wrapper's pid, the sourced rule and its cwd" \
|
|
"$(printf '%s\n' "$out" | grep -q "^BUSY $WRAP sourced test script cwd=$SANDBOX/wrap "; echo $?)"
|
|
out=$(bash "$BUSY" 2>&1); rc=$?
|
|
check "the real /proc has a BUSY line for that wrapper too" \
|
|
"$([ "$rc" != 0 ] && printf '%s\n' "$out" | grep -q "^BUSY $WRAP sourced test script "; echo $?)"
|
|
kill "$WRAP" 2>/dev/null; wait "$WRAP" 2>/dev/null; KIDS=""
|
|
|
|
# External: the wrapper runs node --test as a child (`; :` keeps bash from
|
|
# exec'ing it). The wrapper is skipped and its child is BUSY.
|
|
(cd "$SANDBOX/wrap" && exec bash -c "source $SANDBOX/shell-snapshots/snapshot-x.sh && $SANDBOX/bin/node --test active.test.mjs; :") &
|
|
WRAP=$!; KIDS="$KIDS $WRAP"; kid=""
|
|
for _ in $(seq 50); do
|
|
kid=$(cat "/proc/$WRAP/task/$WRAP/children" 2>/dev/null); kid=${kid%% *}
|
|
[ -n "$kid" ] && has "$(tr '\0' ' ' < "/proc/$kid/cmdline" 2>/dev/null)" "--test active" && break
|
|
sleep 0.1
|
|
done
|
|
KIDS="$KIDS $kid"
|
|
LP="$SANDBOX/lproc2"; mkdir -p "$LP"; ln -s "/proc/$WRAP" "$LP/$WRAP"; [ -n "$kid" ] && ln -s "/proc/$kid" "$LP/$kid"
|
|
out=$(bash "$BUSY" --proc-root "$LP" 2>&1); rc=$?
|
|
check "a live wrapper that runs node --test as a child: exit 1 on the child's BUSY line" \
|
|
"$([ -n "$kid" ] && [ "$rc" = 1 ] && printf '%s\n' "$out" | grep -q "^BUSY $kid node --test "; echo $?)"
|
|
check "the wrapper itself has no BUSY line" "$(! printf '%s\n' "$out" | grep -q "^BUSY $WRAP "; echo $?)"
|
|
# The wrapper exits by itself once its child has; only then are both gone.
|
|
[ -n "$kid" ] && kill "$kid" 2>/dev/null
|
|
for _ in $(seq 50); do kill -0 "$WRAP" 2>/dev/null || break; sleep 0.1; done
|
|
if ! kill -0 "$WRAP" 2>/dev/null; then wait "$WRAP" 2>/dev/null; KIDS=""; fi
|
|
|
|
# R5 (N2): the 13 shapes from the R4 review's Check 2, each a live wrapper
|
|
# that runs the sourced file in its own shell, with no child. S is the
|
|
# snapshot, SF a snapshot that holds `false` (so `||` runs the test), T the
|
|
# test. The sourced file blocks as test-x.sh does above.
|
|
S="$SANDBOX/shell-snapshots/snapshot-x.sh"
|
|
SF="$SANDBOX/shell-snapshots/snapshot-f.sh"
|
|
T="$SANDBOX/scripts/test-x.sh"
|
|
echo false > "$SF"
|
|
mkdir -p "$SANDBOX/wrap/scripts" "$SANDBOX/dir with spaces/scripts"
|
|
for f in wrap/scripts/test-x.sh wrap/gate.sh wrap/mutants.sh "dir with spaces/scripts/test-x.sh"; do
|
|
cp "$T" "$SANDBOX/$f"
|
|
done
|
|
live() { # $1: shape number, $2: expected rule or CLEAR, $3: label, $4: the wrapper's text
|
|
local n=$1 want=$2 label=$3 lp
|
|
rm -f "$SANDBOX/wrap/ready"
|
|
(cd "$SANDBOX/wrap" && exec bash -c "$4") &
|
|
WRAP=$!; KIDS="$KIDS $WRAP"
|
|
for _ in $(seq 50); do [ -e "$SANDBOX/wrap/ready" ] && break; sleep 0.1; done
|
|
kids=$(cat "/proc/$WRAP/task/$WRAP/children") && kids="none$kids"
|
|
lp="$SANDBOX/lproc-s$n"; mkdir -p "$lp"; ln -s "/proc/$WRAP" "$lp/$WRAP"
|
|
out=$(bash "$BUSY" --proc-root "$lp" 2>&1); rc=$?
|
|
if [ "$want" = CLEAR ]; then
|
|
check "live shape $n, $label, no child: exit 0 and CLEAR" \
|
|
"$([ -e "$SANDBOX/wrap/ready" ] && [ "$kids" = none ] && [ "$rc" = 0 ] && [ "$out" = CLEAR ]; echo $?)"
|
|
else
|
|
check "live shape $n, $label, no child: exit 1, $want" \
|
|
"$([ -e "$SANDBOX/wrap/ready" ] && [ "$kids" = none ] && [ "$rc" = 1 ] && ! has "$out" CLEAR \
|
|
&& printf '%s\n' "$out" | grep -q "^BUSY $WRAP $want cwd=$SANDBOX/wrap "; echo $?)"
|
|
fi
|
|
kill "$WRAP" 2>/dev/null; wait "$WRAP" 2>/dev/null; KIDS=""
|
|
}
|
|
live 1 "sourced test script" 'source S && source T' "source $S && source $T"
|
|
live 2 "sourced test script" "source S && .<TAB><TAB>'scripts/test-x.sh'" "source $S && ."$'\t\t'"'scripts/test-x.sh'"
|
|
live 3 "sourced test script" 'source S ; source T ; :' "source $S ; source $T ; :"
|
|
live 4 "sourced test script" 'source SF || source T' "source $SF || source $T"
|
|
live 5 "sourced gate" 'source S && . "./gate.sh"' "source $S && . \"./gate.sh\""
|
|
live 6 "sourced mutant" 'source S && source ./mutants.sh' "source $S && source ./mutants.sh"
|
|
live 7 "sourced test script" 'source S && source ".../dir with spaces/scripts/test-x.sh"' \
|
|
"source $S && source \"$SANDBOX/dir with spaces/scripts/test-x.sh\""
|
|
live 8 "sourced test script" 'source S && "source" T' "source $S && \"source\" $T"
|
|
live 9 "sourced test script" 'source S;source T;:' "source $S;source $T;:"
|
|
live 10 "sourced test script" 'source S;. T;:' "source $S;. $T;:"
|
|
live 11 "sourced test script" 'source "S" && source "T"' "source \"$S\" && source \"$T\""
|
|
live 12 "sourced test script" 'source<TAB>S && source "T"' "source"$'\t'"$S && source \"$T\""
|
|
live 13 "sourced test script" 'source<SPACE><SPACE>S && source "T"' "source $S && source \"$T\""
|
|
# A plain wrapper that blocks the same way but sources no test stays CLEAR.
|
|
live 14 CLEAR 'a plain wrapper, the same blocking read inline' \
|
|
"source $S && exec 3<>\"$SANDBOX/wrap/fifo\" && : > \"$SANDBOX/wrap/ready\" && read -t 60 -u 3 _"
|
|
|
|
echo "---"
|
|
echo "git-guard selftests: $PASS passed, $FAIL failed"
|
|
[ "$FAIL" -eq 0 ]
|