import json import os from pathlib import Path import time from selenium import webdriver from selenium.common.exceptions import WebDriverException from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.firefox.options import Options ROOT = Path(__file__).resolve().parent.parent VVFAT = bool(os.environ.get('XTEINK_VVFAT')) MODE = 'vvfat' if VVFAT else 'raw-sd' ARTIFACT = ROOT / f'_scratch/browser-{MODE}' BAD_MARKERS = ( 'RuntimeError', 'Aborted()', 'index out of bounds', 'unaligned memory access', 'SSI_PERIPHERAL:', 'Guru Meditation', "panic'ed", 'Invalid SD card size', 'FS error', 'SPI:', 'SD:', 'Unexpected response to cmd', 'Error writing to qcow backend', 'write error on host side', ) options = Options() options.add_argument('-headless') options.accept_insecure_certs = True driver = webdriver.Firefox(options=options) driver.set_page_load_timeout(60) def page_state(): try: return driver.execute_script(''' return { status: document.querySelector('#status')?.textContent || '', log: document.querySelector('#log')?.textContent || '', generation: Number(document.querySelector('#frame-generation')?.value || -1), runtimeErrors: window.__xteinkRuntimeErrors || [], wasmHeap: window.__xteinkModule?.HEAPU8?.byteLength || 0, instantiatedTbs: window.__xteinkModule?._wasm_instantiated_tb_count?.() || 0, }; ''') except WebDriverException as error: return { 'status': 'WebDriver lost the page', 'log': '', 'generation': -1, 'runtimeErrors': [str(error)], 'wasmHeap': 0, 'instantiatedTbs': 0, } def failure(state): text = '\n'.join([ state.get('status', ''), state.get('log', ''), *map(str, state.get('runtimeErrors', [])), ]) matched = [marker for marker in BAD_MARKERS if marker in text] if matched: return f'emulator failure detected: {matched}' if state.get('runtimeErrors'): return f'browser runtime error: {state["runtimeErrors"]}' return None def wait_until(predicate, description, timeout): deadline = time.monotonic() + timeout last = None while time.monotonic() < deadline: last = page_state() problem = failure(last) if problem: raise RuntimeError(f'{problem} while waiting for {description}') if predicate(last): return last time.sleep(0.25) raise TimeoutError(f'timed out waiting for {description}; last state: {last}') def wait_stable(quiet=5, timeout=180): deadline = time.monotonic() + timeout state = page_state() last_generation = state['generation'] stable_since = time.monotonic() while time.monotonic() < deadline: state = page_state() problem = failure(state) if problem: raise RuntimeError(f'{problem} while waiting for display stability') if state['generation'] != last_generation: last_generation = state['generation'] stable_since = time.monotonic() elif time.monotonic() - stable_since >= quiet: return state time.sleep(0.25) raise TimeoutError(f'display did not settle; last state: {state}') def save_artifacts(): state = page_state() ARTIFACT.with_suffix('.json').write_text(json.dumps(state, indent=2)) ARTIFACT.with_suffix('.log').write_text(state.get('log', '')) try: driver.save_screenshot(str(ARTIFACT.with_suffix('.png'))) except WebDriverException: pass return state try: suffix = '&vvfat' if VVFAT else '' url = os.environ.get( 'XTEINK_TEST_URL', 'http://127.0.0.1:8180/web/' '?firmware=/_scratch/crosspoint-reader-1.4.1-release-firmware.bin' f'&variant=x3{suffix}', ) # Seed the SD card in OPFS before autoboot; the app no longer auto-seeds. base = url.split('?')[0] driver.get(base) seed = driver.execute_async_script(''' const done = arguments[arguments.length - 1]; import('./sdstore.js').then(async m => { const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; const nl = String.fromCharCode(10, 10); const text = Array(10).fill(lorem).join(nl); await m.clearSdFiles().catch(() => {}); await m.writeSdFile('Test/example.txt', text); done('ok'); }).catch(e => done('err: ' + e)); ''') if seed != 'ok': raise RuntimeError(f'failed to seed SD card: {seed}') driver.get(url) confirm = driver.find_element(By.CSS_SELECTOR, '[data-button="confirm"]') if os.environ.get('XTEINK_INJECT_FAILURE'): driver.execute_script("window.dispatchEvent(new ErrorEvent('error', {message: 'RuntimeError: injected test failure'}))") wait_until( lambda _: confirm.get_dom_attribute('disabled') is None, 'automatic cold boot and enabled controls', 240, ) wait_stable() def press(button, description): driver.execute_script( 'arguments[0].scrollIntoView({block: "center"})', button ) before = wait_stable()['generation'] ActionChains(driver).click_and_hold(button).perform() try: time.sleep(0.3) finally: ActionChains(driver).release(button).perform() wait_until( lambda state: state['generation'] > before, f'frame after {description}', 180, ) return wait_stable() press(confirm, 'opening Browse Files') press(confirm, 'opening Test directory') press(confirm, 'highlighting example.txt') if 'tcgdiff2=open' in url: driver.execute_script('window.xteinkArmTcgDiff2 && window.xteinkArmTcgDiff2()') if 'ramrace=open' in url: driver.execute_script('window.xteinkEnableRamrace && window.xteinkEnableRamrace()') if 'stateflush=open' in url: driver.execute_script('window.xteinkEnableStateflush && window.xteinkEnableStateflush()') if 'tlbguard=open' in url: driver.execute_script('window.xteinkEnableTlbguard && window.xteinkEnableTlbguard()') open_start = time.monotonic() press(confirm, 'opening example.txt') open_seconds = time.monotonic() - open_start down = driver.find_element(By.CSS_SELECTOR, '[data-button="down"]') up = driver.find_element(By.CSS_SELECTOR, '[data-button="up"]') press(down, 'turning to reader page 2') press(down, 'turning to reader page 3') press(up, 'turning back to reader page 2') back = driver.find_element(By.CSS_SELECTOR, '[data-button="back"]') press(back, 'closing the reader') press(confirm, 'reopening example.txt') final_state = wait_stable(quiet=10, timeout=600) problem = failure(final_state) if problem: raise RuntimeError(problem) if final_state['generation'] < 10: raise RuntimeError( f'reader workflow produced too few frames: {final_state["generation"]}' ) print( f'browser {MODE} test passed at frame generation ' f'{final_state["generation"]}, ' f'wasm heap {final_state["wasmHeap"] / 1024 / 1024:.1f} MiB, ' f'open example.txt took {open_seconds:.1f}s' ) except Exception: state = save_artifacts() print(json.dumps(state, indent=2)) raise else: save_artifacts() finally: driver.quit()