79 lines
8.1 KiB
Python
79 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
# Row 163 mutants: each one drops or weakens one behaviour; the named tests must then fail.
|
|
import hashlib, os, shutil, subprocess, sys, signal
|
|
W = '/mnt/storage/scratch/sage-b163'
|
|
OUT = os.environ.get('MUT_OUT', '/mnt/storage/scratch/sage-b163-gate/mutants')
|
|
os.makedirs(OUT, exist_ok=True)
|
|
SR = 'scripts/stack-release.sh'
|
|
T = 'packages/cli/tests/stack-release.test.mjs'
|
|
M = [
|
|
('K1 credential-ref-live dropped', SR, 'if(f===null||phr===null||path.dirname(f)!==phr)fail("credential-ref-live"', 'if(false)fail("credential-ref-live"', T, '^R9 '),
|
|
('K2 tracker-configured dropped', SR, 'if(config.trackers!==undefined)fail(', 'if(false)fail(', T, '^R10 '),
|
|
('K3 data-root-live dropped', SR, 'if(root===real(live))fail(', 'if(false)fail(', T, '^R11 '),
|
|
('K4 sessions-live dropped', SR, 'if(running.length)fail(', 'if(false)fail(', T, '^R11 '),
|
|
('K5 notify-off dropped', SR, 'fail("notify-off",`${nf} is not {', 'void("notify-off",`${nf} is not {', T, '^R12 '),
|
|
('K6 no-session dropped', SR, 'if [ -z "$found" ]; then r_pass no-session', 'if true; then r_pass no-session', T, '^R4 '),
|
|
('K7 copy-match dropped', SR, ' R_GATE=copy-match\n', ' R_GATE=""; return 0\n', T, '^R7 '),
|
|
('K8 role credential swap dropped', SR, 'strip(r?.vars);swap(name,r?.credentials)}', 'strip(r?.vars)}', T, '^R1 '),
|
|
('K9 @sync swap dropped', SR, 'swap("@sync",b.tracker?.sync?.credentials);', '', T, '^R1 '),
|
|
('K10 business tracker strip dropped', SR, '\nstrip(b.vars);\n', '\n', T, '^R1 '),
|
|
('K11 bus-status pid check dropped', SR, 'String(s.host.pid)!==main?`host pid ${s.host.pid} is not ${main}, the host this run', 'false?`host pid ${s.host.pid} is not ${main}, the host this run', T, '^R4 '),
|
|
('K12 host-released-store dropped', SR, 'if [ -e "$R/data/bus/writer.lock" ]; then', 'if false; then', T, '^R4 '),
|
|
('K13 SIGKILL counted as a pass', SR, 'kill -KILL "$pid" 2>/dev/null || true; r_fail "$name-stopped"', 'kill -KILL "$pid" 2>/dev/null || true; r_pass "$name-stopped"', T, '^R5 '),
|
|
('K14 healthz always ok', SR, '.then((r)=>process.exit(r.status===200?0:1),()=>process.exit(1));\' -- "$R_PORT"', '.then(()=>process.exit(0),()=>process.exit(0));\' -- "$R_PORT"', T, '^R6 '),
|
|
('K15 port-reserved dropped', SR, '[ "$((10#$R_PORT))" != "$i" ] || r_abort port-reserved', 'true || r_abort port-reserved', T, '^R8 '),
|
|
('K16 port-in-use dropped', SR, 'r_port_free "$R_PORT" || r_abort port-in-use', 'true || r_abort port-in-use', T, '^R8 '),
|
|
('K17 backup-sums dropped', SR, 'sha256sum -c --quiet ../SHA256SUMS) >/dev/null 2>&1 ||', 'true) >/dev/null 2>&1 ||', T, '^R13 '),
|
|
('K18 copy-without-no-launch dropped', SR, 'grep -qF -- \'"--no-launch"\' "$COPIES/$TO/packages/cli/src/cli.mjs" 2>/dev/null ||', 'true ||', T, '^R13 '),
|
|
('K19 migration skipped', SR, 'if [ "$v" = "$T_SCHEMA" ]; then r_pass migration', 'if true; then r_pass migration', T, '^R2 '),
|
|
('K20 migrate failure ignored', SR, '[ "$rc" = 0 ] && [ "$(tail -n 1 <<< "$out")" = "bus schema $T_SCHEMA ready" ] || r_abort migrate "', 'true || r_abort migrate "', T, '^R3 '),
|
|
('K21 scratch store links the backup', SR, 'cp "$R_BK"/bus/* "$R/data/bus/" &&', 'ln -s "$R_BK"/bus/* "$R/data/bus/" &&', T, '^R1 '),
|
|
('K22 scratch config keeps the live dataRoot', SR, 'c.dataRoot=data;', '', T, '^R1 '),
|
|
('K23 host started without --no-launch', SR, '"$COPIES/$TO/scripts/mosaic" bus start "$BUSINESS" --no-launch >', '"$COPIES/$TO/scripts/mosaic" bus start "$BUSINESS" >', T, '^R1 '),
|
|
('K24 start() launches under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) throw new Refusal("launches-disabled", "this host was started with --no-launch");\n', '', 'packages/cli/tests/launcher.test.mjs', None),
|
|
('K25 session launch request allowed under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) {\n await host.op({ op: "refuse", cap, code: "launches-disabled" })', ' if (false) {\n await host.op({ op: "refuse", cap, code: "launches-disabled" })', 'packages/cli/tests/launcher.test.mjs', None),
|
|
('K26 recover() ends sessions under --no-launch', 'packages/cli/src/launcher.mjs', ' if (!launches) {\n for (const s of leftover) log(', ' if (false) {\n for (const s of leftover) log(', 'packages/cli/tests/launcher.test.mjs', None),
|
|
('K27 ticker runs under --no-launch', 'packages/cli/src/launcher.mjs', 'if (b.launch && launches) ticker', 'if (b.launch) ticker', 'packages/cli/tests/launcher.test.mjs', None),
|
|
('K28 registry writes under --no-launch', 'packages/cli/src/launcher.mjs', 'new Registry(dataRoot, { readOnly: !launches })', 'new Registry(dataRoot)', 'packages/cli/tests/launcher.test.mjs', None),
|
|
('K29 Registry readOnly writes anyway', 'packages/seat/src/session.mjs', ' if (!readOnly) this.#write();', ' this.#write();', 'packages/seat/tests/session.test.mjs', None),
|
|
('K30 Registry readOnly add() allowed', 'packages/seat/src/session.mjs', ' if (this.#readOnly) throw new SeatError("this host keeps no sessions (--no-launch)", 2);\n', '', 'packages/seat/tests/session.test.mjs', None),
|
|
('K31 cli --no-launch not passed to the launcher', 'packages/cli/src/cli.mjs', 'tracker: Boolean(boot.trackers?.[businessId]), launches, ', 'tracker: Boolean(boot.trackers?.[businessId]), ', 'packages/cli/tests/host.test.mjs', None),
|
|
('K32 cli --pm with --no-launch allowed', 'packages/cli/src/cli.mjs', ' if (!launches && parsed.flags.has("--pm")) throw usage(', ' if (false) throw usage(', 'packages/cli/tests/host.test.mjs', None),
|
|
('K33 cli host-up line drops launches disabled', 'packages/cli/src/cli.mjs', '${launches ? "" : ", launches disabled"}', '', 'packages/cli/tests/host.test.mjs', None),
|
|
('K34 backup-unchanged always passes', SR, 'if [ "$(r_tree_sums "$R_BK")" = "$(cat "$R/backup.before")" ]; then r_pass backup-unchanged', 'if true; then r_pass backup-unchanged', T, '^R14 '),
|
|
('K35 live-config-unchanged always passes', SR, 'if [ "$(sha256sum "$LIVE_CFG" "$LIVE_BIZ" 2>&1)" = "$(cat "$R/live.before")" ]; then', 'if true; then', T, '^R14 '),
|
|
('K36 credential-ref-live accepts an env ref', SR, 'if(ref?.env!==undefined)fail("credential-ref-live"', 'if(false)fail("credential-ref-live"', T, '^R9 '),
|
|
('K37 data-root-scratch mismatch dropped', SR, 'if(root!==real(scratch))fail(', 'if(false)fail(', T, '^R11 '),
|
|
('K38 data-root-scratch missing root dropped', SR, 'if(root===null)fail("data-root-scratch"', 'if(false)fail("data-root-scratch"', T, '^R11 '),
|
|
('K39 no-session ignores a launches dir', SR, '[ ! -e "$R/data/launches" ] || found+=" launches"', 'true || found+=" launches"', T, '^R4 '),
|
|
('K40 no INT/TERM/HUP trap', SR, ' trap \'r_fail signal "the rehearsal was interrupted"; r_end\' INT TERM HUP\n', '', T, '^R15 '),
|
|
('K41 read-only Registry writes', 'packages/seat/src/session.mjs', ' if (this.#readOnly) return;\n', '', 'packages/seat/tests/session.test.mjs', None),
|
|
]
|
|
sha = lambda p: hashlib.sha256(open(p, 'rb').read()).hexdigest()
|
|
env = dict(os.environ, TMPDIR='/mnt/storage/scratch/sb163t')
|
|
results = []
|
|
ONLY = set(sys.argv[1:])
|
|
for name, f, old, new, test, pat in M:
|
|
if ONLY and name.split()[0] not in ONLY: continue
|
|
p = os.path.join(W, f); bak = os.path.join(OUT, os.path.basename(f) + '.orig')
|
|
shutil.copy2(p, bak)
|
|
if sha(bak) != sha(p): sys.exit(f'backup of {p} failed')
|
|
src = open(p).read()
|
|
if src.count(old) != 1: sys.exit(f'{name}: pattern count {src.count(old)} in {f}')
|
|
open(p, 'w').write(src.replace(old, new))
|
|
cmd = ['node', '--test'] + (['--test-name-pattern', pat] if pat else []) + [test]
|
|
log = os.path.join(OUT, name.split()[0] + '.log')
|
|
try:
|
|
with open(log, 'w') as lf:
|
|
pr = subprocess.Popen(cmd, cwd=W, env=env, stdout=lf, stderr=subprocess.STDOUT, start_new_session=True)
|
|
try: rc = pr.wait(timeout=600)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(pr.pid, signal.SIGKILL); pr.wait(); rc = 'timeout'
|
|
finally:
|
|
shutil.copy2(bak, p)
|
|
if sha(bak) != sha(p): sys.exit(f'restore of {p} failed')
|
|
killed = rc != 0
|
|
results.append((name, killed, rc))
|
|
print(f'{"KILLED" if killed else "SURVIVED"} {name} (rc {rc})', flush=True)
|
|
print('ALL_KILLED' if all(k for _, k, _ in results) else 'SOME_SURVIVED')
|