73 lines
6.6 KiB
Python
73 lines
6.6 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/native-browser-work');ROOT.mkdir(exist_ok=True);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=9334','--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:9334/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:8081/native_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(Path('/tmp/native-api-cases.json').read_text())
|
|
parity=js('(async()=>{const cases='+json.dumps(cases,separators=(',',':'))+';for(const c of cases){const a=await NativeAltAI.simulate(NativeAltAI.model,Uint8Array.from(c.pixels));if(a.prediction!==c.prediction||!a.counts.every((v,i)=>v===c.hidden[i])||!a.output_counts.every((v,i)=>v===c.outputs[i]))throw Error("native parity "+c.index);}return{samples:cases.length,hidden_exact:true,output_exact:true};})()',True)
|
|
def settle():
|
|
return js("new Promise((resolve,reject)=>{let n=0;const t=setInterval(()=>{const answer=document.querySelector('#answer').textContent;if(answer!=='…'&&answer!=='—'){clearInterval(t);resolve({answer,result:FastMNIST.lastResult});}else if(document.querySelector('#error').textContent||++n>100){clearInterval(t);reject(Error(document.querySelector('#error').textContent||'timeout'));}},50);})",True)
|
|
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
|
|
js("document.querySelector('#gallery button').click()");settle();gallery=js("({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/'native-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})
|
|
settle();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':[]})
|
|
settle();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/'native-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}),500))",True)
|
|
assert stable['blank'] and stable['answer']=='—',stable
|
|
timing={'scope':'real server native KNP subprocess; not browser-only','milliseconds_gallery':gallery.get('milliseconds')}
|
|
result={'ready':ready,'parity':parity,'blank':blank,'gallery2534':gallery,'mouse_drawn1':mouse,'touch_drawn1_mobile390':touch,'clear_after_debounce':stable,'inference_timing_headless':timing,'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)
|