Files
2026-10-18 14:24:42 -05:00

51 lines
2.8 KiB
Python

#!/usr/bin/env python3
# Running cases requires Sage's separate full test grant. --preflight is read-only.
import json, os, pathlib, signal, subprocess, sys, re
ROOT=pathlib.Path('/mnt/storage/src/mosaic-stack-rocko-164-r1')
OUT=pathlib.Path(__file__).resolve().parent/'mutants'
cases=json.loads((OUT.parent/'mutant-sites.json').read_text())
for c in cases:
assert (ROOT / c['path']).read_text().count(c['old']) == 1, c['check']
if '--preflight' in sys.argv:
print('PREFLIGHT_OK', len(cases)); sys.exit(0)
OUT.mkdir(exist_ok=True)
resume = next((int(a.split('=')[1]) for a in sys.argv if a.startswith('--resume=')), 1)
rows = json.loads((OUT / 'results.json').read_text())[:resume - 1] if resume > 1 else []
def stop_tree(pid):
# Git's own runner creates process groups too. Capture descendants before killing the parent.
descendants = {pid}
for _ in range(8):
before = set(descendants)
for entry in pathlib.Path('/proc').iterdir():
if not entry.name.isdigit(): continue
try:
fields = (entry / 'stat').read_text().split(') ', 1)[1].split()
if int(fields[1]) in descendants: descendants.add(int(entry.name))
except (OSError, ValueError, IndexError): pass
if before == descendants: break
for child in descendants:
try: os.killpg(child, signal.SIGKILL)
except ProcessLookupError: pass
for c in cases[resume - 1:]:
file = ROOT / c['path']; original = file.read_bytes(); process = None
try:
file.write_text(original.decode().replace(c['old'], c['new']))
with (OUT / (c['check'] + '.log')).open('w') as log:
process = subprocess.Popen(['node', '--test', '--test-name-pattern', re.escape(c['killer']), c['test']], cwd=ROOT,
env={**os.environ, 'DOCKER_HOST': 'unix:///nonexistent.sock', 'TMPDIR': '/mnt/storage/scratch/rocko-164/tmp'}, stdout=log,
stderr=subprocess.STDOUT, start_new_session=True)
try: rc = process.wait(timeout=180)
except subprocess.TimeoutExpired: stop_tree(process.pid); process.wait(); rc = 'timeout'
content = (OUT / (c['check'] + '.log')).read_text()
checker = ['node', '--check', c['path']] if c['path'].endswith(('.mjs', '.js')) else ['bash', '-n', c['path']]
syntax = subprocess.run(checker, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, timeout=10)
invalid = syntax.returncode != 0 or 'ERR_MODULE_NOT_FOUND' in content
verdict = 'INVALID' if invalid else 'TIMEOUT' if rc == 'timeout' else 'SURVIVED' if rc == 0 else 'KILLED'
rows.append({k: c[k] for k in ['check', 'path', 'test', 'killer']} | {'result': verdict, 'exit': rc})
(OUT / 'results.json').write_text(json.dumps(rows, indent=2) + '\n')
print(c['check'], verdict, rc, flush=True)
finally:
if process is not None and process.poll() is None: stop_tree(process.pid); process.wait()
file.write_bytes(original)
if verdict != 'KILLED': sys.exit(1)