91 lines
5.2 KiB
Python
91 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Run only on Sage CLEAR. Mutate isolated HEAD archives; never edit the checkout.
|
|
|
|
A green control run is required before each negative result counts as a kill.
|
|
Usage: python3 agents/beakley/work/queue-217/mutants.py OUTPUT_DIRECTORY
|
|
"""
|
|
from pathlib import Path
|
|
import io
|
|
import hashlib
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
OUT = Path(sys.argv[1]).resolve()
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|
WRITER = "packages/business/src/organization.mjs"
|
|
BROKER = "packages/bus/src/broker.mjs"
|
|
CLIENT = "packages/bus/src/client.mjs"
|
|
ATOMIC = "if (check) this.#event(s, 'action.allowed', { action: 'message.checked', message: mid, check }, mid, checkEventId(mid));"
|
|
REENTER = "return this.#reenter(cap, s, { verb: 'message.send', args }, check);"
|
|
SPLIT = """const result = this.#reenter(cap, s, { verb: 'message.send', args }, check);
|
|
if (check) this.#store.transaction(() => this.#event(s, 'action.allowed', { action: 'message.checked', message: result.id, check }, result.id, checkEventId(result.id)));
|
|
return result;"""
|
|
MUTANTS = [
|
|
("M1-writer-fence", [(WRITER, " replyLimitsFence(old, document, actor);", ""),
|
|
(WRITER, " replyLimitsFence(old, written, actor);", "")],
|
|
["packages/business/tests/reply-limits.test.mjs"]),
|
|
("M2-incoming-human", [(WRITER, "actor === old.human", "actor === document.human")],
|
|
["packages/business/tests/reply-limits.test.mjs"]),
|
|
("M3-split-check-transaction", [(BROKER, ATOMIC, ""), (BROKER, REENTER, SPLIT)],
|
|
["packages/bus/tests/citation-guard.test.mjs"]),
|
|
("M5-hide-library-unavailable", [("packages/library/src/guard.mjs",
|
|
"refuse('citation-unavailable', 'The library is busy or unavailable. Retry the answer when the library is available.');",
|
|
"served = null;")], ["packages/bus/tests/citation-availability.test.mjs"]),
|
|
("M4-drop-client-reason", [(CLIENT, "new BusError(r.error, typeof r.reason === 'string' ? r.reason.slice(0, 300) : undefined)", "new BusError(r.error)")],
|
|
["packages/bus/tests/citation-guard.test.mjs"]),
|
|
]
|
|
|
|
archive = subprocess.check_output(["git", "archive", "HEAD", "packages", "roles", "scripts", "tools", "docs", "package.json", "RELEASE"], cwd=ROOT)
|
|
summary = []
|
|
selected = set(sys.argv[2:])
|
|
product_files = [line.split(' ', 1)[1] for line in (ROOT / 'agents/beakley/work/queue-217/manifest.r5.sha256').read_text().splitlines()]
|
|
def hashes(base, files):
|
|
return {file: hashlib.sha256((base / file).read_bytes()).hexdigest() for file in files}
|
|
original_products = hashes(ROOT, product_files)
|
|
for name, changes, tests in MUTANTS:
|
|
if selected and name not in selected:
|
|
continue
|
|
with tempfile.TemporaryDirectory(prefix="row217-mutant-") as directory:
|
|
work = Path(directory)
|
|
with tarfile.open(fileobj=io.BytesIO(archive)) as source:
|
|
source.extractall(work, filter="data")
|
|
modules = ROOT / "node_modules"
|
|
if modules.is_symlink():
|
|
raise SystemExit('root node_modules must be a local install, never a canonical link')
|
|
if modules.exists():
|
|
os.symlink(modules.resolve(), work / "node_modules", target_is_directory=True)
|
|
cmd = ["timeout", "-k", "10", "120", "node", "--test", *tests]
|
|
with (OUT / f"{name}-control.log").open("w") as log:
|
|
control = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=120)
|
|
if control.returncode:
|
|
raise SystemExit(f"{name}: control RED, no mutant result claimed")
|
|
originals = {file: (work / file).read_bytes() for file, _, _ in changes}
|
|
try:
|
|
for file, before, after in changes:
|
|
path = work / file
|
|
text = path.read_text()
|
|
if text.count(before) != 1:
|
|
raise SystemExit(f"{name}: expected exactly one mutation anchor in {file}")
|
|
path.write_text(text.replace(before, after))
|
|
with (OUT / f"{name}.log").open("w") as log:
|
|
mutant = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=140)
|
|
if mutant.returncode != 1:
|
|
raise SystemExit(f"{name}: expected assertion failure exit 1, got {mutant.returncode}")
|
|
finally:
|
|
for file, content in originals.items():
|
|
(work / file).write_bytes(content)
|
|
assert all((work / file).read_bytes() == content for file, content in originals.items())
|
|
assert hashes(ROOT, product_files) == original_products, 'checkout product hashes changed'
|
|
(OUT / f"{name}-restored.sha256").write_text(''.join(f'{digest} {file}\n' for file, digest in sorted(original_products.items())))
|
|
with (OUT / f"{name}-restored-green.log").open("w") as log:
|
|
restored = subprocess.run(cmd, cwd=work, stdout=log, stderr=subprocess.STDOUT, timeout=140)
|
|
if restored.returncode:
|
|
raise SystemExit(f"{name}: restored control RED")
|
|
summary.append(f"{name}: killed (control green, mutant exit {mutant.returncode}, hashes restored, restored green)")
|
|
(OUT / "summary.txt").write_text("\n".join(summary) + "\n")
|
|
print(summary[-1], flush=True)
|