#!/usr/bin/env python3 # Row 97 (#1591) mutants. Each edit must match exactly once. Every run starts in its own process # group and the group is killed when the run ends or times out, so no node or chromium is left # behind. The mutated file is restored from the bytes read before the edit, and every touched # file's sha256 is checked against its start at the end. # Usage: mutants.py [timeout-seconds] import hashlib, os, signal, subprocess, sys, time WT, OUT = sys.argv[1], sys.argv[2] TIMEOUT = int(sys.argv[3]) if len(sys.argv) > 3 else 120 A = 'packages/webui/src/public/app.js' B = 'packages/webui/src/public/bus.js' BOTH = ['tests/board-held.test.mjs', 'tests/browser-edge.test.mjs'] EDGE = ['tests/browser-edge.test.mjs'] # (name, file, old, new, tests, per-run timeout) M = [ ('13 schedule polls with the toggle off', A, 'if (!paused && machine && !out()) timer', 'if (!paused && !out()) timer', BOTH, TIMEOUT), ('27 bus schedule polls signed out', B, 'if (!paused() && !out()) timer', 'if (!paused()) timer', BOTH, TIMEOUT), ('12 refresh reads with the toggle off', A, 'if (busy || !machine || out()) return;', 'if (busy || out()) return;', BOTH, TIMEOUT), ('14 refresh without its signed-out guard', A, 'if (busy || !machine || out()) return;', 'if (busy || !machine) return;', BOTH, TIMEOUT), ('26 bus render without its signed-out guard', B, " clearTimeout(timer);\n if (out()) return;\n", " clearTimeout(timer);\n", BOTH, TIMEOUT), ('19 toggle on reads nothing', A, "fresh(); refresh(); } else hideMachine();", "fresh(); } else hideMachine();", EDGE, 30), ] env = dict(os.environ, TMPDIR=os.environ.get('TMPDIR', '/mnt/storage/scratch/tmp')) sha = lambda p: hashlib.sha256(open(os.path.join(WT, p), 'rb').read()).hexdigest() start = {p: sha(p) for p in (A, B)} def run(tests, limit): t0 = time.time() p = subprocess.Popen(['node', '--test', *tests], cwd=os.path.join(WT, 'packages/webui'), env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, start_new_session=True, text=True) try: out, _ = p.communicate(timeout=limit) code = p.returncode except subprocess.TimeoutExpired: code, out = 'timeout', '' finally: try: os.killpg(p.pid, signal.SIGKILL) except ProcessLookupError: pass try: p.communicate(timeout=5) except Exception: pass return code, round(time.time() - t0, 1) with open(OUT, 'w') as f: def say(s): print(s); f.write(s + '\n'); f.flush() say(f'worktree {WT}') say(f'start {time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}') killed = 0 for name, path, old, new, tests, limit in M: full = os.path.join(WT, path) orig = open(full, 'rb').read() src = orig.decode() if src.count(old) != 1: say(f'{name}: NO MATCH ({src.count(old)}), skipped'); continue try: open(full, 'w').write(src.replace(old, new)) code, secs = run(tests, limit) finally: open(full, 'wb').write(orig) # A timeout is a stall, not a kill: the file never exited by itself. dead = code not in (0, 'timeout') killed += dead verdict = 'killed' if dead else 'STALLED' if code == 'timeout' else 'ALIVE' say(f'{name}: {verdict} (exit {code}, {secs}s, {" ".join(t.split("/")[-1] for t in tests)})') ok = all(sha(p) == start[p] for p in (A, B)) say(f'killed {killed} of {len(M)}; sources restored byte for byte: {ok}') sys.exit(0 if ok else 1)