our_knp/mnist-altai-native/browser_check.py

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)