79 lines
5.8 KiB
Python
79 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Isolated room mutants. Own worktree dependencies, assertion kills only, killpg after each run."""
|
|
import argparse, hashlib, json, os, signal, subprocess, tarfile, tempfile, time
|
|
from pathlib import Path
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
UI = 'packages/webui/src/public/chat.js'
|
|
HTTP = 'packages/webui/src/chat-http.mjs'
|
|
BROKER = 'packages/bus/src/broker.mjs'
|
|
PLUGIN = 'packages/plugins/channel/webui/index.mjs'
|
|
SEAM = 'packages/webui/src/chat.mjs'
|
|
BROWSER_TEST = 'packages/webui/tests/room-browser.test.mjs'
|
|
HTTP_TEST = 'packages/webui/tests/room-http.test.mjs'
|
|
BUS_TEST = 'packages/bus/tests/room-check.test.mjs'
|
|
PLUGIN_TEST = 'packages/plugins/tests/room-channel.test.mjs'
|
|
MUTANTS = [
|
|
('M01-plugin', PLUGIN, ' "room.open",\n', '', PLUGIN_TEST),
|
|
('M02-seam-body', SEAM, "channel('room.send', { project, body })", "channel('room.send', { project, body: body.trim() })", HTTP_TEST),
|
|
('M03-page-cursor', HTTP, 'value.at(-1)?.ids.at(-1)', 'value.at(-1)?.id', HTTP_TEST),
|
|
('M04-room-status', HTTP, 'Object.keys(ROOM_LINES).map(code => [code, 409])', 'Object.keys(ROOM_LINES).map(code => [code, 400])', HTTP_TEST),
|
|
('M05-strict-body', HTTP, "!exact(v, ['body']) || ", '', HTTP_TEST),
|
|
('M06-receipt-type', HTTP, "typeof t.instance === 'string' && ", '', HTTP_TEST),
|
|
('M07-room-check', BROKER, 'return entries.map((m) => this.#withCheck(m));', 'return entries;', BUS_TEST),
|
|
('M08-room-last', BROKER, "['thread.messages', 'room.read'].includes(request.verb)", "['thread.messages'].includes(request.verb)", BUS_TEST),
|
|
('M09-browser-body', UI, 'request(`rooms/${c.id}/messages`, { body })', 'request(`rooms/${c.id}/messages`, { body: body.trim() })', BROWSER_TEST),
|
|
('M10-members', UI, 'catalog.rows.filter(m => threads.rows.some(t => t.project === id && t.instance === m.instance && t.name === m.instance))', 'catalog.rows', BROWSER_TEST),
|
|
('M11-checked-render', UI, 'messageBody({ ...m, from_role:', 'messageBody({ ...m, check: null, from_role:', BROWSER_TEST),
|
|
('M12-leading-tags', UI, 'let body = input.trimStart();', 'let body = input;', BROWSER_TEST),
|
|
('M13-readonly', UI, 'if (!roomOwner() || !c.canSend || !c.roomReady || out())', 'if (!roomOwner() || !c.roomReady || out())', BROWSER_TEST),
|
|
('M14-uncertainty', HTTP, 'write ? sendOutcome(err) :', "write ? { code: 'invalid-request', sent: false } :", HTTP_TEST),
|
|
('M15-pending', UI, ' if (s.pending) return;', '', BROWSER_TEST),
|
|
('M16-body-dedup', UI, 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.id, m]))', 'new Map((after ? [...s.rows, ...page.rows] : page.rows).map(m => [m.body, m]))', BROWSER_TEST),
|
|
]
|
|
def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest()
|
|
def main():
|
|
p=argparse.ArgumentParser(); p.add_argument('--output',type=Path,required=True);p.add_argument('--only');a=p.parse_args()
|
|
names=set(a.only.split(',')) if a.only else {m[0] for m in MUTANTS}
|
|
assert names <= {m[0] for m in MUTANTS}
|
|
assert not subprocess.check_output(['git','status','--porcelain','--','packages'],cwd=ROOT,text=True).strip(),'Commit tested packages first'
|
|
assert (ROOT/'node_modules').is_dir() and not (ROOT/'node_modules').is_symlink(),'Local install required'
|
|
before={f:sha(ROOT/f) for f in {m[1] for m in MUTANTS}}
|
|
head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip()
|
|
a.output.mkdir(parents=True,exist_ok=True);results=[]
|
|
with tempfile.TemporaryDirectory(prefix='fethry266-mutants-') as temp:
|
|
temp=Path(temp);archive=temp/'source.tar'
|
|
with archive.open('wb') as stream:
|
|
subprocess.run(['git','archive','HEAD','packages','roles','contracts','tasks','missions','package.json','RELEASE'],cwd=ROOT,stdout=stream,check=True)
|
|
for name,target,old,new,test in MUTANTS:
|
|
if name not in names:continue
|
|
tree=temp/name;tree.mkdir()
|
|
with tarfile.open(archive) as bundle:bundle.extractall(tree,filter='data')
|
|
(tree/'node_modules').symlink_to(ROOT/'node_modules',target_is_directory=True)
|
|
path=tree/target;source=path.read_text();hits=source.count(old)
|
|
assert hits==1,(name,'anchor hits',hits)
|
|
path.write_text(source.replace(old,new,1));limit=240 if test==BROWSER_TEST else 120
|
|
cmd=['timeout','-k','10',str(limit),'node','--test','--test-reporter=tap','--test-concurrency=1','--test-timeout=30000',test]
|
|
signals=[];started=time.monotonic()
|
|
with (a.output/(name+'.tap')).open('w') as log:
|
|
child=subprocess.Popen(cmd,cwd=tree,env={**os.environ,'MOSAIC_REQUIRE_EDITOR_BUILD':'1'},stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
|
|
try:code=child.wait(timeout=limit+15)
|
|
except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL');code=child.wait()
|
|
finally:
|
|
try:
|
|
os.killpg(child.pid,signal.SIGTERM);signals.append('TERM');time.sleep(.2)
|
|
try:os.killpg(child.pid,signal.SIGKILL);signals.append('KILL')
|
|
except ProcessLookupError:pass
|
|
except ProcessLookupError:pass
|
|
path.write_text(source)
|
|
restored=sha(path)==hashlib.sha256(source.encode()).hexdigest();unchanged=before=={f:sha(ROOT/f) for f in before}
|
|
assert restored and unchanged,(name,'restore changed')
|
|
output=(a.output/(name+'.tap')).read_text()
|
|
killed=code==1 and 'ERR_ASSERTION' in output and 'not ok ' in output and "failureType: 'hookFailed'" not in output and '# cancelled 0\n' in output and '# skipped 0\n' in output
|
|
result={'mutant':name,'test':test,'exit':code,'killed':killed,'hits':hits,'restored':restored,'candidateUnchanged':unchanged,'seconds':round(time.monotonic()-started,3),'cleanupSignals':signals}
|
|
results.append(result);print(json.dumps(result),flush=True)
|
|
if not killed:break
|
|
report={'source':head,'selected':sorted(names),'candidateUnchanged':before=={f:sha(ROOT/f) for f in before},'results':results}
|
|
(a.output/'summary.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
if len(results)!=len(names) or not all(r['killed'] for r in results):raise SystemExit(1)
|
|
if __name__=='__main__':main()
|