#!/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-171') 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-171/tmp', 'MOSAIC_CONFIG': '/mnt/storage/scratch/rocko-171/no-live-config.json'}, 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)