app
service-kicad
Public Made by Adomby adom
Shared headless KiCad 10: DRC, ERC, SVG/Gerber/STEP export, Altium library conversion, symbol/footprint/3D-model lookup. Every Adom tool shells to this CLI instead of calling the HTTP API directly.
master
John Lauer
Merge PR #99: Add complete-project DRC with input hashes and explicit rule probes
2b18ad1
1mo ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
#!/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()