master
John Lauer Merge PR #99: Add complete-project DRC with input hashes and explicit rule probes 2b18ad1 1mo ago
#!/usr/bin/env python3
"""Reproducible HTTP/CLI tests with a protocol double. Never native KiCad DRC."""
import argparse,hashlib,json,os,socket,subprocess,tempfile,time,urllib.request
from pathlib import Path
ap=argparse.ArgumentParser();ap.add_argument('--bin-dir',type=Path,default=Path('target/debug'));args=ap.parse_args();bin_dir=args.bin_dir.resolve()
with tempfile.TemporaryDirectory(prefix='service-kicad-protocol-test-') as temp:
 root=Path(temp);fixtures=root/'inputs';fixtures.mkdir();stub=root/'fake-kicad'
 stub.write_text('''#!/usr/bin/env python3
import json,sys
from pathlib import Path
if sys.argv[1:]==['version']:
 print('TEST-DOUBLE-NOT-NATIVE-KICAD');raise SystemExit(0)
a=sys.argv[1:];assert a[:2]==['pcb','drc'];b=Path(a[2]);assert b.exists() and b.with_suffix('.kicad_pro').exists();r=b.with_suffix('.kicad_dru').read_text()
if 'MALFORMED_TEST_MARKER' in r:
 print('TEST DOUBLE: error parsing custom rules',file=sys.stderr);raise SystemExit(2)
v=[{'severity':'error','type':'disallowed','description':'TEST DOUBLE: named-negative-control'}] if 'named-negative-control' in r else []
Path(a[a.index('-o')+1]).write_text(json.dumps({'kicad_version':'TEST-DOUBLE-NOT-NATIVE-KICAD','violations':v}))
''');stub.chmod(0o700)
 sock=socket.socket();sock.bind(('127.0.0.1',0));port=sock.getsockname()[1];sock.close()
 url=f'http://127.0.0.1:{port}';env=dict(os.environ,PORT=str(port),KICAD_CLI=str(stub),SERVICE_KICAD_URL=url)
 log=(root/'server.log').open('w');server=subprocess.Popen([str(bin_dir/'service-kicad-api')],env=env,stdout=log,stderr=log)
 results=[]
 try:
  for _ in range(50):
   if server.poll() is not None:raise RuntimeError('Test API terminated')
   try:
    with urllib.request.urlopen(url+'/health',timeout=.2) as response:health=json.load(response)
    assert health['kicad_version']=='TEST-DOUBLE-NOT-NATIVE-KICAD';break
   except OSError:time.sleep(.1)
  else:raise RuntimeError('Test API did not become ready')
  def run(case,stem,rule,success,error=None):
   for ext,text in [('kicad_pcb','(board)'),('kicad_pro','{}'),('kicad_dru',rule)]:
    (fixtures/f'{stem}.{ext}').write_text(text)
   out=root/f'{case}.json'
   command=[str(bin_dir/'service-kicad'),'pcb','drc-project',str(fixtures),'--board',stem+'.kicad_pcb','--expect-rule','named-negative-control','--out',str(out)]
   p=subprocess.run(command,env=env,capture_output=True,text=True,timeout=15)
   assert (p.returncode==0)==success,(case,p.returncode,p.stderr)
   v=json.loads(out.read_text());assert v['ok']==success
   if success:
    assert v['nativeKicadVersion']=='TEST-DOUBLE-NOT-NATIVE-KICAD'
    assert len(v['inputManifest'])==3
    for f in v['inputManifest']:assert f['sha256']==hashlib.sha256((fixtures/f['path']).read_bytes()).hexdigest()
   else:assert v['errorCode']==error
   results.append({'case':case,'exitCode':p.returncode,'errorCode':v.get('errorCode'),'nativeKicadExecuted':False})
  run('exact-inputs','board','named-negative-control',True)
  run('encoded-name','board & plus+#','named-negative-control',True)
  run('missing-rule','missing-probe','(version 1)',False,'expected_rule_not_observed')
  run('error-forwarding','malformed','MALFORMED_TEST_MARKER',False,'native_drc_failed')
  p=subprocess.run([str(bin_dir/'service-kicad'),'pcb','drc',str(fixtures/'board.kicad_pcb'),'--format','json'],env=env,capture_output=True,text=True,timeout=15)
  assert 'PROJECT_RULES_OMITTED' in p.stderr
  results.append({'case':'single-file-omission-hint','hintSeen':True,'nativeKicadExecuted':False})
  print(json.dumps({'mode':'Protocol test double, not native DRC','serviceVersion':health['version'],'serviceGitSha':health['git_sha'],'results':results},indent=2))
 finally:
  server.terminate()
  try:server.wait(timeout=3)
  except subprocess.TimeoutExpired:server.kill();server.wait(timeout=3)
  log.close()