#!/usr/bin/env python3 # Row 117 (#1611): the brief's five mutants of the fetched outcome, one per check. # m1 fetches for a third reason: a failed status becomes a fetched reason. # m2 merges after the fetch in a fetched clone. # m3 drops the fetch for a fetched clone. # m4 reports the clone's HEAD as the origin commit. # m5 fetches before the gitlink check (and so before the root checks too). # Each runs the FULL seat suite (node --test tests/*.test.mjs in packages/seat) # and restores the source from the .orig copy whatever happens. # Usage: mutants.py import os, shutil, subprocess, sys, signal WT, LOGS = sys.argv[1], sys.argv[2] SRC = 'packages/seat/src/checkout.mjs' FETCH = '[...inClone(dest), "fetch", "-q", "--no-tags", "--no-recurse-submodules", "origin"]' MERGE = '[...inClone(dest), "merge", "-q", "--ff-only", "--no-edit", `refs/remotes/origin/${at.branch}`]' MUTANTS = { 'm1': (SRC, ' if (!status.ok) return { outcome: "kept", reason: "status-failed" };\n', ' if (!status.ok) reason = "status-failed";\n'), 'm2': (SRC, ' if (reason) {\n', f' if (reason) {{\n await g({MERGE});\n'), 'm3': (SRC, f' if (!(await g({FETCH})).ok)', f' if (!reason && !(await g({FETCH})).ok)'), 'm4': (SRC, 'origin: { branch: at.branch, commit: origin.ok ? origin.out.trim() : null } };', 'origin: { branch: at.branch, commit: before } };'), 'm5': (SRC, ' // ls-files reads only the index: no filter, hook or submodule runs.\n', f' await g({FETCH});\n // ls-files reads only the index: no filter, hook or submodule runs.\n'), } os.makedirs(LOGS, exist_ok=True) env = dict(os.environ, TMPDIR=os.path.expanduser('~/darkwing-scratch/r117/tmp')) for name, (rel, cur, new) in MUTANTS.items(): src = os.path.join(WT, rel) text = open(src).read() if text.count(cur) != 1: sys.exit(f'STOP {name}: the target text in {rel} is not unique or has moved') if os.path.exists(src + '.orig'): sys.exit(f'STOP {rel}.orig already exists') log_path = os.path.join(LOGS, f'mutant-{name}.log') shutil.copy2(src, src + '.orig') try: open(src, 'w').write(text.replace(cur, new)) with open(log_path, 'w') as out: p = subprocess.Popen('node --test tests/*.test.mjs', shell=True, cwd=os.path.join(WT, 'packages/seat'), stdout=out, stderr=subprocess.STDOUT, env=env, start_new_session=True) try: rc = p.wait(timeout=600) except subprocess.TimeoutExpired: os.killpg(p.pid, signal.SIGKILL) p.wait() rc = 'timeout' finally: shutil.move(src + '.orig', src) restored = open(src).read() == text log = open(log_path).read() counts = [l for l in log.splitlines() if l.startswith(('ℹ pass', 'ℹ fail', 'ℹ skipped'))] failed = [l.strip() for l in log.splitlines() if l.lstrip().startswith('✖') and 'failing tests' not in l] print(f'mutant {name} rc={rc} {" ".join(counts)} restored={restored}') for f in sorted(set(failed)): print(' ' + f) print('KILLED' if rc != 0 else 'SURVIVED') if not restored: sys.exit(f'STOP {rel} was not restored')