#!/usr/bin/env bash # mutate-push-guard.sh -- regenerate the README's mutation table from MEASUREMENT. # # WHY THIS EXISTS. The README carried a hand-written mutation table quoting # "32/32" style results. Those numbers were true when typed and went stale in # silence as the suite grew. A README is what a reader trusts when the tool # misbehaves, so a confidently-wrong one is worse than none. Every number the # README prints about mutation now comes out of this script. # # TWO WAYS A MUTATION RUN LIES, AND THE GUARD FOR EACH: # # 1. THE ANCHOR NO LONGER MATCHES. The mutant is never applied, the suite is # green, and the report says SURVIVED -- which is the same word a real # coverage gap gets. Guarded by grep-before-mutate (ANCHOR MISSING). # # 2. THE ANCHOR MATCHES PROSE. This one bit me on the first run: my # "revert refuse-on-missing-config" mutant landed inside the usage() # heredoc, edited a help string, changed no behaviour, and duly reported # SURVIVED. I nearly recorded a documentation edit as an uncovered branch. # A MUTATION THAT CANNOT CHANGE BEHAVIOUR IS NOT A SURVIVING MUTANT, IT IS # A NON-MEASUREMENT -- and a non-measurement reported as a result is the # same defect as the vacuous test it is supposed to be hunting. # Guarded by prose_range(): anchors inside usage() are a loud refusal. # # 3. THE ANCHOR IS AMBIGUOUS. replace(...,1) silently picks the first match, # which may not be the branch named in the label. Guarded by an exact # occurrence count. set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TARGET="$HERE/push-guard.sh" SUITE="$HERE/test-push-guard.sh" BAK="$(mktemp)"; cp "$TARGET" "$BAK" trap 'cp "$BAK" "$TARGET"; rm -f "$BAK"' EXIT # --- where the prose lives: usage() { ... EOF --------------------------------- PROSE_LO="$(grep -n '^usage() {' "$BAK" | head -1 | cut -d: -f1)" PROSE_HI="$(awk -v lo="$PROSE_LO" 'NR > lo && /^EOF$/ { print NR; exit }' "$BAK")" if [[ -z "$PROSE_LO" || -z "$PROSE_HI" ]]; then echo "!! cannot locate the usage() heredoc -- the prose guard would be inert; refusing" >&2 exit 1 fi printf 'prose (usage heredoc) is lines %s-%s -- anchors there are refused, not scored\n\n' \ "$PROSE_LO" "$PROSE_HI" rc_all=0 KILLED=0; SURVIVED=0 declare -a ROWS=() mutate() { local name="$1" find="$2" repl="$3" # Count and locate in python so MULTI-LINE anchors work. They are required: # two unrelated branches in this file are both the single line # `if (( status != 0 )); then`, and a one-line anchor cannot say which one a # result belongs to. Guessing would attribute a kill to the wrong branch. local loc; loc="$(python3 - "$BAK" "$find" <<'LOCPY' import sys s = open(sys.argv[1]).read(); find = sys.argv[2] n = s.count(find) print(n, (s[:s.index(find)].count("\n") + 1) if n else 0) LOCPY )" local n="${loc%% *}" ln="${loc##* }" if (( n == 0 )); then printf ' !! ANCHOR MISSING %-46s NOT APPLIED -- result would be meaningless\n' "$name" rc_all=1; return fi if (( n != 1 )); then printf ' !! ANCHOR AMBIGUOUS %-46s %d matches -- refusing to guess which branch\n' "$name" "$n" rc_all=1; return fi if (( ln >= PROSE_LO && ln <= PROSE_HI )); then printf ' !! ANCHOR IS PROSE %-46s line %d is inside usage() -- not a branch\n' "$name" "$ln" rc_all=1; return fi python3 - "$BAK" "$TARGET" "$find" "$repl" <<'PY' import sys src, dst, find, repl = sys.argv[1:5] open(dst, "w").write(open(src).read().replace(find, repl, 1)) PY local out line failed passed total out="$("$SUITE" 2>&1)" line="$(printf '%s\n' "$out" | grep -E 'needles: [0-9]+ passed' | tail -1)" failed="$(printf '%s\n' "$line" | sed -n 's/.*, \([0-9]*\) failed.*/\1/p')" passed="$(printf '%s\n' "$line" | sed -n 's/.*: \([0-9]*\) passed.*/\1/p')" if [[ -z "$failed" || -z "$passed" ]]; then printf ' !! NO TALLY %-46s suite produced no needle count\n' "$name" rc_all=1; cp "$BAK" "$TARGET"; return fi total=$(( passed + failed )) if (( failed > 0 )); then printf ' KILLED L%-5s %-46s %s/%s fail\n' "$ln" "$name" "$failed" "$total" ROWS+=("| \`$name\` (L$ln) | $failed/$total fail | killed |") KILLED=$(( KILLED + 1 )) else printf ' SURVIVED L%-5s %-46s 0/%s fail <-- UNCOVERED BRANCH\n' "$ln" "$name" "$total" ROWS+=("| \`$name\` (L$ln) | 0/$total fail | **SURVIVED** |") SURVIVED=$(( SURVIVED + 1 )); rc_all=1 fi cp "$BAK" "$TARGET" } echo "=== push-guard mutation run ===" mutate "json decision requirement bypassed" \ ' if (( ${#JSON_PATHS[@]} == 0 )); then' ' if false; then' mutate "opt-out accepted with no written reason" \ 'if not isinstance(reason, str) or not reason.strip():' 'if False:' mutate "committed re-read of the opt-out skipped" \ ' [[ "$CFG_MODE" == "none" ]] || return 0' ' return 0' mutate "untracked config honoured as an opt-out" \ ' if [[ -z "$rel" ]]; then' ' if false; then' mutate "staged-but-uncommitted opt-out honoured" \ ' if [[ -z "$cmode" ]]; then' ' if false; then' mutate "committed SYMLINK config honoured" \ ' if [[ "$cmode" == "120000" ]]; then' ' if false; then' mutate "unparseable committed config ignored" \ ' if (( cstatus != 0 )); then' ' if false; then' mutate "local-only opt-out (HEAD says ON) honoured" \ ' if [[ "$cmode_val" != "none" ]]; then' ' if false; then' mutate "empty MERGE exempted" \ ' if [[ "$all_same" == yes ]]; then' ' if false; then' mutate "empty ROOT exempted" \ 'if [[ -z "$(git diff-tree --root -r --name-only --no-commit-id HEAD)" ]]; then' \ 'if false; then' mutate "--since-head ancestry check removed" \ 'if ! git merge-base --is-ancestor "$since_head" "$head"; then' 'if false; then' mutate "staged-file enumeration ignores git failure" \ "$(printf 'if (( status != 0 )); then\n local msg')" \ "$(printf 'if false; then\n local msg')" mutate "malformed config degrades to absent instead of refusing" \ "$(printf 'if (( status != 0 )); then\n fail "$EX_CONFIG"')" \ "$(printf 'if false; then\n fail "$EX_CONFIG"')" BASE="$("$SUITE" 2>&1 | grep -E 'needles: [0-9]+ passed' | tail -1)" printf '\nunmodified: %s\n' "$BASE" printf '%d killed, %d survived\n' "$KILLED" "$SURVIVED" printf '\n--- README TABLE (paste verbatim) ---\n' printf '| mutation | suite result | verdict |\n|---|---|---|\n' printf '| *unmodified* | %s | baseline |\n' \ "$(printf '%s' "$BASE" | sed 's/push-guard needles: //')" printf '%s\n' "${ROWS[@]}" exit "$rc_all"