Files
FreeCADWasmDev/tools/run_smoke_business_records.py

132 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Resume a bounded business group one durable requirement at a time."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import time
import signal
from business_acceptance import ROOT, requirements, fingerprint, digest, successful_result
def write(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix('.tmp')
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
temporary.replace(path)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--module', required=True)
parser.add_argument('--ids', nargs='+', required=True)
parser.add_argument('--summarize-only', action='store_true',
help='Audit cumulative per-ID evidence without running cases or replacing original fingerprints')
args = parser.parse_args()
def stop(_signum, _frame):
raise KeyboardInterrupt
signal.signal(signal.SIGTERM, stop)
directory = Path(os.environ.get('FREECAD_SMOKE_RUN_DIR', str(ROOT / 'docs/current/comprehensive-smoke-20260918'))).resolve()
path = directory / ('records-' + args.module + '.json')
state = json.loads(path.read_text()) if path.exists() else {'records': {}, 'history': []}
rows = {row['id']: row for row in requirements()}
contract = ROOT / 'docs/business-acceptance' / (args.module + '.json')
configuration = json.loads(contract.read_text())
scripts = [ROOT / configuration['command'][1], ROOT / 'tools/run_business_acceptance.py']
for name in configuration.get('dependencies', []) + configuration.get('caseDependencies', []):
dependency = ROOT / name
if dependency.is_dir():
scripts.extend(p for p in dependency.rglob('*') if p.is_file() and '__pycache__' not in p.parts)
elif dependency.is_file():
scripts.append(dependency)
script_digest = hashlib.sha256(json.dumps({str(p): digest(p) for p in scripts}, sort_keys=True).encode()).hexdigest()
if args.summarize_only:
if state.get('current'):
print(json.dumps({'status': 'incomplete', 'current': state['current'],
'reason': 'Finish or resume the current ID before collecting its group.'}))
return 1
outcomes = []
evidence_cache = {}
for record_id in args.ids:
old = state['records'].get(record_id, {})
report = old.get('report', {})
paths = report.get('evidencePaths') or [report.get('evidencePath')]
proof = None
for name in paths:
if not name:
continue
evidence = (ROOT / name).resolve()
if not evidence.is_relative_to(ROOT) or not evidence.is_file():
continue
if evidence not in evidence_cache:
evidence_cache[evidence] = json.loads(evidence.read_text())
for result in evidence_cache[evidence].get('results', []):
if result.get('capabilityId') == record_id and successful_result(result):
proof = {'path': str(evidence.relative_to(ROOT)), 'sha256': digest(evidence),
'executedAt': evidence_cache[evidence].get('executedAt')}
current = hashlib.sha256((fingerprint(rows[record_id]) + script_digest).encode()).hexdigest()
outcomes.append({'id': record_id, 'status': 'pass' if old.get('status') == 'pass' and proof else 'fail',
'fingerprint': old.get('fingerprint'), 'matchesCurrentInputs': old.get('fingerprint') == current,
'evidence': proof})
failed = [row['id'] for row in outcomes if row['status'] != 'pass']
summary = {'status': 'fail' if failed else 'pass', 'passed': len(outcomes) - len(failed),
'remaining': failed, 'records': outcomes,
'basis': 'Cumulative smoke evidence plus targeted repairs; historical fingerprints are preserved, not recertified as current.',
'currentInputMatches': sum(row['matchesCurrentInputs'] for row in outcomes),
'checkpoint': str(path)}
write(directory / ('summary-' + args.module + '.json'), summary)
print(json.dumps(summary))
return bool(failed)
for record_id in args.ids:
key = hashlib.sha256((fingerprint(rows[record_id]) + script_digest).encode()).hexdigest()
old = state['records'].get(record_id)
if old and old.get('fingerprint') == key:
print(json.dumps({'id':record_id,'status':'reused','result':old['status']}),flush=True)
continue
if old:
state['history'].append(old)
state['current'] = record_id
write(path, state)
started = time.time()
stem = hashlib.sha256(record_id.encode()).hexdigest()[:16] + '-' + key[:12]
logfile = directory / ('record-' + stem + '.log')
command = [sys.executable, str(ROOT / 'tools/run_business_acceptance.py'), '--module', args.module,
'--ids', record_id, '--reuse', '--validate-deferred', '--retry-failed']
# The inner owner enforces its per-case timeout and reaps browser children.
with logfile.open('w') as output:
child = subprocess.Popen(command, cwd=ROOT, stdout=output, stderr=subprocess.STDOUT)
try:
child.wait()
finally:
if child.poll() is None:
child.terminate()
try: child.wait(timeout=10)
except subprocess.TimeoutExpired:
child.kill(); child.wait()
run = child
report = {}
for line in reversed(logfile.read_text().splitlines()):
try:
report = json.loads(line)
break
except ValueError:
continue
row = {'id': record_id, 'fingerprint': key, 'status': 'pass' if run.returncode == 0 else 'fail',
'seconds': round(time.time()-started,3), 'log':logfile.name, 'report':report}
state['records'][record_id] = row
state.pop('current',None)
write(path,state)
print(json.dumps({'id':record_id,'status':row['status'],'seconds':row['seconds']}),flush=True)
failed = [record_id for record_id in args.ids if state['records'][record_id]['status'] != 'pass']
print(json.dumps({'status':'pass' if not failed else 'fail', 'passed':len(args.ids)-len(failed),
'remaining':failed,'checkpoint':str(path)}))
return bool(failed)
if __name__ == '__main__':
raise SystemExit(main())