71 lines
6.8 KiB
Python
71 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Bounded Chromium CDP parity and real mouse/touch smoke test."""
|
|
import base64,json,os,shutil,socket,subprocess,tempfile,time,urllib.request
|
|
from pathlib import Path
|
|
import websocket
|
|
ROOT=Path('/tmp/mnist-fast-work');profile=tempfile.mkdtemp(prefix='mnist-cdp-');errors=[];counter=0
|
|
log=(ROOT/'browser-check.log').open('w');browser=subprocess.Popen(['chromium','--headless','--no-sandbox','--disable-gpu','--remote-debugging-port=9333','--remote-allow-origins=*','--user-data-dir='+profile,'about:blank'],stdout=log,stderr=log)
|
|
ws=None
|
|
try:
|
|
for _ in range(50):
|
|
try:
|
|
pages=json.load(urllib.request.urlopen('http://127.0.0.1:9333/json',timeout=1));break
|
|
except Exception:time.sleep(.1)
|
|
else:raise RuntimeError('Chromium CDP unavailable')
|
|
ws=websocket.create_connection(next(p['webSocketDebuggerUrl'] for p in pages if p['type']=='page'),timeout=20)
|
|
def call(method,params=None):
|
|
global counter
|
|
counter+=1;want=counter;ws.send(json.dumps({'id':want,'method':method,'params':params or {}}))
|
|
while True:
|
|
message=json.loads(ws.recv())
|
|
if message.get('method')=='Runtime.exceptionThrown':errors.append(message['params'])
|
|
if message.get('id')==want:
|
|
if 'error' in message:raise RuntimeError(message['error'])
|
|
return message.get('result',{})
|
|
def js(expression,await_promise=False):
|
|
result=call('Runtime.evaluate',{'expression':expression,'returnByValue':True,'awaitPromise':await_promise})
|
|
if 'exceptionDetails' in result:raise RuntimeError(result['exceptionDetails'])
|
|
return result['result'].get('value')
|
|
call('Page.enable');call('Runtime.enable');call('Page.navigate',{'url':'http://10.9.8.230:8080/fast_viz.html'})
|
|
ready=js("new Promise((resolve,reject)=>{let n=0;const t=setInterval(()=>{if(window.FastMNIST){clearInterval(t);FastMNIST.ready.then(m=>resolve({version:m.version,steps:m.steps})).catch(reject);}else if(++n>100){clearInterval(t);reject(Error('page not ready'));}},50);})",True)
|
|
cases=json.loads((ROOT/'parity-cases.json').read_text())['cases']
|
|
parity=js('''(()=>{const cases='''+json.dumps(cases,separators=(',',':'))+''';let normalized=0,counts=0,scores=0,predictions=0;for(const item of cases){const decode=s=>Uint8Array.from(atob(s),x=>x.charCodeAt(0));const pixels=FastMNIST.normalizeGray(decode(item.raw));if(!pixels.every((v,i)=>v===decode(item.normalized)[i]))throw Error('normalization '+item.test_index);normalized++;const out=FastMNIST.simulate(FastMNIST.model,pixels);if(!out.counts.every((v,i)=>v===item.counts[i]))throw Error('IF counts '+item.test_index);counts++;if(!out.scores.every((v,i)=>v===item.scores[i]))throw Error('scores '+item.test_index);scores++;if(out.prediction!==item.prediction)throw Error('prediction '+item.test_index);predictions++;}return{normalized,counts,scores,predictions};})()''')
|
|
blank=js("(()=>{document.querySelector('#clear').click();return {answer:document.querySelector('#answer').textContent,blank:FastMNIST.lastResult.blank,scores:document.querySelector('#scores').children.length};})()")
|
|
assert blank=={'answer':'—','blank':True,'scores':0},blank
|
|
gallery=js("(()=>{document.querySelector('#gallery button').click();return{answer:document.querySelector('#answer').textContent,prediction:FastMNIST.lastResult.prediction,sample:document.querySelector('#sample').textContent};})()")
|
|
assert gallery['prediction']==3,gallery
|
|
shot=call('Page.captureScreenshot',{'format':'png','captureBeyondViewport':True});(ROOT/'fast-viz-desktop.png').write_bytes(base64.b64decode(shot['data']))
|
|
# Mouse: draw a simple1 with genuine browser inputevents.
|
|
js("document.querySelector('#clear').click();document.querySelector('#draw').scrollIntoView({block:'center'});")
|
|
rect=js("(()=>{let r=document.querySelector('#draw').getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height}})()")
|
|
x=rect['x']+.52*rect['w'];y0=rect['y']+.18*rect['h'];y1=rect['y']+.82*rect['h']
|
|
call('Input.dispatchMouseEvent',{'type':'mousePressed','x':x,'y':y0,'button':'left','clickCount':1})
|
|
for i in range(1,16):call('Input.dispatchMouseEvent',{'type':'mouseMoved','x':x,'y':y0+(y1-y0)*i/15,'button':'left','buttons':1})
|
|
call('Input.dispatchMouseEvent',{'type':'mouseReleased','x':x,'y':y1,'button':'left','clickCount':1})
|
|
mouse=js("({prediction:FastMNIST.lastResult.prediction,blank:FastMNIST.lastResult.blank,ink:FastMNIST.lastNormalized.reduce((a,b)=>a+b,0)})")
|
|
assert mouse['prediction']==1 and not mouse['blank'],mouse
|
|
call('Emulation.setDeviceMetricsOverride',{'width':390,'height':844,'deviceScaleFactor':1,'mobile':True})
|
|
call('Emulation.setTouchEmulationEnabled',{'enabled':True,'maxTouchPoints':1})
|
|
js("document.querySelector('#clear').click();document.querySelector('#draw').scrollIntoView({block:'center'});")
|
|
rect=js("(()=>{let r=document.querySelector('#draw').getBoundingClientRect();return{x:r.x,y:r.y,w:r.width,h:r.height}})()")
|
|
x=rect['x']+.5*rect['w'];y0=rect['y']+.18*rect['h'];y1=rect['y']+.82*rect['h']
|
|
call('Input.dispatchTouchEvent',{'type':'touchStart','touchPoints':[{'x':x,'y':y0,'id':1}]})
|
|
for i in range(1,16):call('Input.dispatchTouchEvent',{'type':'touchMove','touchPoints':[{'x':x,'y':y0+(y1-y0)*i/15,'id':1}]})
|
|
call('Input.dispatchTouchEvent',{'type':'touchEnd','touchPoints':[]})
|
|
touch=js("({prediction:FastMNIST.lastResult.prediction,blank:FastMNIST.lastResult.blank,ink:FastMNIST.lastNormalized.reduce((a,b)=>a+b,0),overflow:document.documentElement.scrollWidth>innerWidth})")
|
|
assert touch['prediction']==1 and not touch['blank'] and not touch['overflow'],touch
|
|
shot=call('Page.captureScreenshot',{'format':'png','captureBeyondViewport':True});(ROOT/'fast-viz-mobile.png').write_bytes(base64.b64decode(shot['data']))
|
|
# Clearing immediately afterpointer inputmustsurvive an olddebounce timer.
|
|
js("document.querySelector('#clear').click()")
|
|
stable=js("new Promise(r=>setTimeout(()=>r({blank:FastMNIST.lastResult.blank,answer:document.querySelector('#answer').textContent}),150))",True)
|
|
assert stable['blank'] and stable['answer']=='—',stable
|
|
timing=js("(()=>{let x=Uint8Array.from(atob('"+cases[0]["normalized"]+"'),c=>c.charCodeAt(0));let times=[];for(let i=0;i<120;i++){const r=FastMNIST.simulate(FastMNIST.model,x);if(i>=20)times.push(r.milliseconds);}times.sort((a,b)=>a-b);return{samples:100,median_ms:times[50],p95_ms:times[95]};})()")
|
|
result={'ready':ready,'parity':parity,'blank':blank,'gallery2534':gallery,'mouse_drawn1':mouse,'touch_drawn1_mobile390':touch,'clear_after_debounce':stable,'inference_timing_headless':{**timing,'sample':'officialtest2534normalized','scope':'networkonly; excludescanvasnormalization/DOM','hardware':'localheadlessChromiumnotuserdevice'},'javascript_errors':errors}
|
|
assert not errors,errors
|
|
(ROOT/'browser-check.json').write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(result))
|
|
finally:
|
|
if ws:ws.close()
|
|
browser.terminate()
|
|
try:browser.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:browser.kill();browser.wait()
|
|
log.close();shutil.rmtree(profile,ignore_errors=True)
|