68 lines
4.7 KiB
Python
68 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# Dewey row 74 mutants: single-site edits in the mutant worktree (never the build tree),
|
|
# each run against the preview and serve tests, then restored from the index.
|
|
import os, subprocess, sys
|
|
|
|
R = os.path.expanduser('~/dewey-scratch/r74')
|
|
T = R + '/mut'
|
|
OUT = R + '/out'
|
|
SERVE = 'packages/webui/src/serve.mjs'
|
|
CLI = 'packages/webui/src/cli.mjs'
|
|
TESTS = ['node', '--test', 'packages/webui/tests/preview.test.mjs', 'packages/webui/tests/serve.test.mjs']
|
|
|
|
MUTANTS = [
|
|
('partial-allowed', SERVE, "if (given.length !== 3) throw", "if (given.length < 1) throw", TESTS),
|
|
('host-not-lowercased', SERVE, "const host = String(publicHost).toLowerCase(), header", "const host = String(publicHost), header", TESTS),
|
|
('header-not-lowercased', SERVE, "header = String(authHeader).toLowerCase();", "header = String(authHeader);", TESTS),
|
|
('ip-allowed', SERVE, "if (!HOSTNAME.test(host) || isIP(host)) throw", "if (!HOSTNAME.test(host)) throw", TESTS),
|
|
('host-header-allowed', SERVE, "['host', 'origin'].includes(header)", "['origin'].includes(header)", TESTS),
|
|
('user-comma-allowed', SERVE, "const USER = /^[\\x21-\\x2b\\x2d-\\x7e]{1,256}$/;", "const USER = /^[\\x21-\\x7e]{1,256}$/;", TESTS),
|
|
('empty-object-serves', SERVE, "if (preview !== null && !(preview = previewOptions(", "if (preview !== null && false && !(preview = previewOptions(", TESTS),
|
|
('named-case-sensitive', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '') === preview.host", TESTS),
|
|
('named-prefix', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '').toLowerCase().startsWith(preview.host)", TESTS),
|
|
('named-hostname-only', SERVE, "(req.headers.host ?? '').toLowerCase() === preview.host", "(req.headers.host ?? '').toLowerCase().split(':')[0] === preview.host", TESTS),
|
|
('origin-http-allowed', SERVE, "(named ? `https://${preview.host}` : `http://${req.headers.host}`)", "(named ? `http://${preview.host}` : `http://${req.headers.host}`)", TESTS),
|
|
('origin-public-on-loopback', SERVE, "req.headers.origin !== (named ? `https://${preview.host}`", "req.headers.origin !== `https://${preview?.host}` && req.headers.origin !== (named ? `https://${preview.host}`", TESTS),
|
|
('header-check-skipped', SERVE, " if (named) {\n // One value", " if (false) {\n // One value", TESTS),
|
|
('two-values-allowed', SERVE, "values.length !== 1 || values[0] !== preview.user", "!values.includes(preview.user)", TESTS),
|
|
('value-prefix', SERVE, "values[0] !== preview.user", "!values[0].startsWith(preview.user)", TESTS),
|
|
('value-case-insensitive', SERVE, "values[0] !== preview.user", "values[0].toLowerCase() !== preview.user.toLowerCase()", TESTS),
|
|
('header-check-raw-name', SERVE, "req.headersDistinct[preview.header]", "req.headersDistinct[preview.header.toUpperCase()]", TESTS),
|
|
('readonly-named-only', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (named && req.method !== 'GET' && req.method !== 'HEAD') return", TESTS),
|
|
('readonly-post-only', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (preview !== null && req.method === 'POST') return", TESTS),
|
|
('readonly-always', SERVE, "if (preview !== null && req.method !== 'GET' && req.method !== 'HEAD') return", "if (req.method !== 'GET' && req.method !== 'HEAD') return", TESTS),
|
|
('cli-preview-dropped', CLI, "bus, reads, preview: options.preview });", "bus, reads });", TESTS),
|
|
('cli-checked-late', CLI, " options.preview = previewOptions(preview);\n", "", TESTS),
|
|
]
|
|
|
|
def run(cmd):
|
|
try:
|
|
p = subprocess.run(cmd, cwd=T, capture_output=True, text=True, timeout=120, env={**os.environ, 'TMPDIR': R + '/tmp'})
|
|
except subprocess.TimeoutExpired as e:
|
|
return 'timeout', str(e.stdout or '')
|
|
return p.returncode, p.stdout + p.stderr
|
|
|
|
want = sys.argv[1:]
|
|
results = []
|
|
for name, f, old, new, tests in MUTANTS:
|
|
if want and name not in want:
|
|
continue
|
|
path = os.path.join(T, f)
|
|
src = open(path).read()
|
|
if src.count(old) != 1:
|
|
results.append(f'{name} BAD-SITE ({src.count(old)} matches)')
|
|
print(results[-1], flush=True)
|
|
continue
|
|
open(path, 'w').write(src.replace(old, new))
|
|
try:
|
|
rc, out = run(tests)
|
|
finally:
|
|
subprocess.run(['git', 'checkout', '--', f], cwd=T, check=True)
|
|
open(f'{OUT}/mut-{name}.txt', 'w').write(out)
|
|
results.append(f"{name} {'killed (timeout)' if rc == 'timeout' else 'killed' if rc else 'SURVIVED'}")
|
|
print(results[-1], flush=True)
|
|
rc, diff = run(['git', 'diff', '--stat'])
|
|
print('worktree diff after:', diff.strip() or 'clean')
|
|
killed = sum(1 for r in results if ' killed' in r)
|
|
print(f'summary {killed} of {len(results)} killed')
|