213 lines
7.5 KiB
Python
213 lines
7.5 KiB
Python
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()
|