82 lines
5.1 KiB
Python
82 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
# Row 294 mutants M1-M11 (BUILD.md "Mutants"). Each mutant edits the worktree (every old text must
|
|
# match exactly once), runs its killing file alone under timeout -k 10 300 with the TAP reporter,
|
|
# then restores with git checkout and checks the tree is clean. Killed = the named killing test(s)
|
|
# report "not ok". M1 also needs G5 "ok" in the same run (Duckworth C1).
|
|
import json, os, re, subprocess, sys, time
|
|
|
|
WT = '/mnt/storage/scratch/launchpad-r294/wt'
|
|
OUT = '/mnt/storage/scratch/launchpad-r294/run4'
|
|
GU = ('packages/webui', 'tests/granted-user.test.mjs')
|
|
AB = ('packages/cli', 'tests/access-boot.test.mjs')
|
|
|
|
CHAT_CALL = "verb === 'plugins' ? owner('plugins', {}) : call(verb, args)"
|
|
MUTANTS = [
|
|
('M1', [('packages/webui/src/serve.mjs', "if (!userRoute(path, req.method)) return json(res, 403, { error: 'owner-only'",
|
|
"if (false) return json(res, 403, { error: 'owner-only'")], GU, ['R6 G4'], ['R6 G5']),
|
|
('M2', [('packages/webui/src/sso.mjs', "if (s.user !== undefined && !users?.check(s)) {", "if (s.user !== undefined && false) {")], GU, ['R6 G3'], []),
|
|
('M3', [('packages/webui/src/public/user.html', "but not your messages.", "but not your replies.")], GU, ['R6 G6'], []),
|
|
('M4', [('packages/cli/src/access.mjs', "const peers = (system.federation?.peers ?? []).map((p) => p.id);", "const peers = [];")], AB,
|
|
['R6 a configured peer id', 'R6 B1'], []),
|
|
('M5', [('packages/webui/src/access.mjs', "return user && live(doc, user.id, b) ? user.id : null;", "return user ? user.id : null;")], GU, ['R6 G2'], []),
|
|
('M6', [('packages/webui/src/user-bus.mjs', "env: { ...rest, MOSAIC_BUS_USER_NONCE", "env: { ...env, MOSAIC_BUS_USER_NONCE")], GU, ['R6 G8'], []),
|
|
('M7', [('packages/cli/src/config.mjs', " if (access) config.access = access;\n", "")], AB, ['R6 B1'], []),
|
|
('M8', [('packages/webui/src/chat.mjs', CHAT_CALL,
|
|
"verb === 'plugins' ? owner('plugins', {}) : verb === 'thread.list' ? owner(verb, args) : call(verb, args)")], GU, ['R6 G9'], []),
|
|
('M9', [('packages/cli/src/config.mjs', " warn(`access file refused at boot (${code}), no granted user binds until it is fixed: ${file}`);\n", "")],
|
|
AB, ['R6 B3'], []),
|
|
('M10', [('packages/webui/src/console-bus.mjs', "return session => consoleBus(transport(session.user), { send, owner });",
|
|
"return session => consoleBus(transport(session.user), { send, owner: (v, a) => owner(v, { ...a, sub: session.sub }) });")], GU, ['R6 G9'], []),
|
|
('M11', [('packages/webui/src/chat.mjs', "const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => " + CHAT_CALL + " });",
|
|
"let last = {};\n const dispatch = createChannelCaller({ name: 'webui', call: owner === null ? call : (verb, args) => verb === 'plugins' ? owner('plugins', last) : call(verb, args) });"),
|
|
('packages/webui/src/chat.mjs', " try { return await dispatch(verb, args); }", " try { last = args; return await dispatch(verb, args); }")], GU, ['R6 G9'], []),
|
|
]
|
|
|
|
def git(*a):
|
|
return subprocess.run(['git', '-C', WT, *a], capture_output=True, text=True, check=True).stdout
|
|
|
|
def restore(files):
|
|
if files: git('checkout', '--', *sorted(files))
|
|
if git('status', '--porcelain'): raise SystemExit('worktree not clean after restore')
|
|
|
|
def results(tap):
|
|
# Top-level TAP test lines: "ok N - name" / "not ok N - name" at no indentation.
|
|
return {m.group(2): m.group(1) == 'ok' for m in re.finditer(r'^(ok|not ok) \d+ - (.*)$', tap, re.M)}
|
|
|
|
def main():
|
|
only = sys.argv[1:]
|
|
if git('status', '--porcelain'): raise SystemExit('worktree not clean at start')
|
|
env = dict(os.environ)
|
|
summary, bad = [], 0
|
|
for mid, edits, (pkg, test), killers, greens in MUTANTS:
|
|
if only and mid not in only: continue
|
|
files = set()
|
|
try:
|
|
for path, old, new in edits:
|
|
p = os.path.join(WT, path); s = open(p).read()
|
|
if s.count(old) != 1: raise SystemExit(f'{mid}: anchor count {s.count(old)} in {path}')
|
|
open(p, 'w').write(s.replace(old, new)); files.add(path)
|
|
diff = git('diff')
|
|
t0 = time.time()
|
|
r = subprocess.run(['timeout', '-k', '10', '300', 'node', '--test', '--test-reporter=tap', test],
|
|
cwd=os.path.join(WT, pkg), capture_output=True, text=True, env=env)
|
|
dt = round(time.time() - t0, 1)
|
|
finally:
|
|
restore(files)
|
|
open(f'{OUT}/{mid}.diff', 'w').write(diff)
|
|
open(f'{OUT}/{mid}.tap', 'w').write(r.stdout + '\n# stderr\n' + r.stderr)
|
|
res = results(r.stdout)
|
|
hit = lambda pre: [n for n, ok in res.items() if n.startswith(pre)]
|
|
red = [n for n, ok in res.items() if not ok]
|
|
killed = r.returncode not in (0, 124, 137) and all(any(not res[n] for n in hit(k)) for k in killers) \
|
|
and all(hit(g) and all(res[n] for n in hit(g)) for g in greens)
|
|
bad += not killed
|
|
line = f'{mid} rc={r.returncode} seconds={dt} killed={killed} red={red}'
|
|
print(line, flush=True)
|
|
summary.append({'mutant': mid, 'rc': r.returncode, 'seconds': dt, 'killed': killed, 'red': red,
|
|
'killers': killers, 'greens': greens})
|
|
json.dump(summary, open(f'{OUT}/mutants.json', 'w'), indent=1)
|
|
sys.exit(1 if bad else 0)
|
|
|
|
main()
|