116 lines
5.0 KiB
Python
116 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Run explicit row278 changes only under Sage's grant. --static runs no tests."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import signal
|
|
import subprocess
|
|
import time
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
REPO = HERE.parents[3]
|
|
|
|
def digest(data):
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
def busy():
|
|
p = subprocess.run(['bash', '/mnt/storage/scratch/sage-busy-now.sh'], text=True, capture_output=True)
|
|
if p.returncode or p.stdout.splitlines()[-1:] != ['CLEAR']:
|
|
raise RuntimeError('busy refusal: ' + p.stdout + p.stderr)
|
|
|
|
def run(command, path, seconds=330):
|
|
busy()
|
|
if command[:2] == ['node', '--test']:
|
|
command = ['timeout', '-k', '10', '300', *command]
|
|
started = time.monotonic()
|
|
command_file = path.with_suffix('.command.json')
|
|
command_file.write_text(json.dumps(command))
|
|
# Keep command text out of wrapper argv: the busy guard scans every argv.
|
|
# launch.py execs the structured command. bash owns a pipefail/tee pipeline.
|
|
p = subprocess.Popen(['bash','-o','pipefail','-c',
|
|
'python3 "$1" "$2" 2>&1 | tee "$3"', '--',str(HERE/'launch.py'),str(command_file),str(path)],
|
|
cwd=REPO,start_new_session=True)
|
|
expired = False
|
|
try:
|
|
code = p.wait(timeout=seconds)
|
|
expired = code in (124, 137)
|
|
except subprocess.TimeoutExpired:
|
|
expired = True
|
|
os.killpg(p.pid, signal.SIGKILL)
|
|
code = p.wait()
|
|
finally:
|
|
try:
|
|
os.killpg(p.pid, signal.SIGKILL)
|
|
except ProcessLookupError:
|
|
pass
|
|
return dict(code=code, timeout=expired, seconds=round(time.monotonic()-started, 3), log=str(path))
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('--static', action='store_true')
|
|
ap.add_argument('--grant')
|
|
ap.add_argument('--out', type=Path)
|
|
args = ap.parse_args()
|
|
expected = json.loads((HERE/'CANDIDATE-SHAS.json').read_text())
|
|
for name, sha in expected.items():
|
|
assert digest((REPO/name).read_bytes()) == sha, name
|
|
definitions = json.loads((HERE/'changes.json').read_text())
|
|
checked = []
|
|
for d in definitions:
|
|
original = (REPO/d['file']).read_bytes()
|
|
count = original.count(d['old'].encode())
|
|
if count != 1:
|
|
raise RuntimeError(f"{d['id']}: target occurs {count} times")
|
|
changed = original.replace(d['old'].encode(), d['new'].encode(), 1)
|
|
checked.append(dict(id=d['id'], file=d['file'], occurrences=count,
|
|
before=digest(original), after=digest(changed)))
|
|
if args.static:
|
|
print(json.dumps(checked, indent=2))
|
|
return
|
|
if not args.grant or not args.out:
|
|
ap.error('execution needs --grant and --out')
|
|
if not os.environ.get('TMPDIR', '').startswith('/mnt/storage/scratch/tmp/g'):
|
|
raise RuntimeError('TMPDIR must be the granted scratch directory')
|
|
status = subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO)
|
|
args.out.mkdir(parents=True, exist_ok=True)
|
|
results = dict(grant=args.grant, static=checked, bases={}, changes=[])
|
|
failures = 0
|
|
for d in definitions:
|
|
key = d['killer']+'|'+d['pattern']
|
|
command = ['node','--test','--test-reporter=tap','--test-name-pattern',d['pattern'],d['killer']]
|
|
if key not in results['bases']:
|
|
base = run(command, args.out/(d['id']+'-base.log'))
|
|
results['bases'][key] = base
|
|
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
|
|
if base['code'] != 0 or base['seconds'] > 30:
|
|
raise RuntimeError('base failed or exceeded 30 seconds: '+d['id'])
|
|
target = REPO/d['file']
|
|
original = target.read_bytes()
|
|
try:
|
|
target.write_bytes(original.replace(d['old'].encode(),d['new'].encode(),1))
|
|
busy()
|
|
pre = subprocess.run(['node','--check',str(target)],capture_output=True)
|
|
if pre.returncode:
|
|
raise RuntimeError('invalid syntax: '+d['id'])
|
|
result = run(command,args.out/(d['id']+'.log'))
|
|
result.update(id=d['id'], base_seconds=results['bases'][key]['seconds'],
|
|
outcome='timeout' if result['timeout'] else 'killed' if result['code'] != 0 else 'survived')
|
|
results['changes'].append(result)
|
|
if result['timeout']:
|
|
raise RuntimeError('timeout requires Sage/Mos classification: '+d['id'])
|
|
if result['outcome'] != 'killed':
|
|
failures += 1
|
|
finally:
|
|
target.write_bytes(original)
|
|
assert target.read_bytes() == original
|
|
assert subprocess.check_output(['git','status','--porcelain','--untracked-files=no'],cwd=REPO) == status
|
|
(args.out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
|
|
if failures >= 1:
|
|
raise RuntimeError('first unexpected failure: end the granted window')
|
|
print(json.dumps(results,indent=2))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|