132 lines
6.6 KiB
Python
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())
|