528 lines
18 KiB
Bash
Executable File
528 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
chromium_bin=${CHROMIUM:-}
|
|
if [[ -z "$chromium_bin" ]]; then
|
|
for candidate in chromium chromium-browser google-chrome; do
|
|
if command -v "$candidate" >/dev/null 2>&1; then
|
|
chromium_bin=$(command -v "$candidate")
|
|
break
|
|
fi
|
|
done
|
|
elif [[ ! -x "$chromium_bin" ]] && command -v "$chromium_bin" >/dev/null 2>&1; then
|
|
chromium_bin=$(command -v "$chromium_bin")
|
|
fi
|
|
|
|
missing=()
|
|
minimum_sections=${BROWSER_SMOKE_MIN_SECTIONS:-50}
|
|
if ! [[ "$minimum_sections" =~ ^[1-9][0-9]*$ ]]; then
|
|
echo "BROWSER_SMOKE_MIN_SECTIONS must be a positive integer: $minimum_sections" >&2
|
|
exit 1
|
|
fi
|
|
|
|
require_command() {
|
|
local command_name=$1
|
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
|
missing+=("$command_name")
|
|
fi
|
|
}
|
|
|
|
require_file() {
|
|
local file_path=$1
|
|
if [[ ! -f "$file_path" ]]; then
|
|
missing+=("$file_path")
|
|
elif [[ ! -r "$file_path" || ! -s "$file_path" ]]; then
|
|
missing+=("readable non-empty $file_path")
|
|
fi
|
|
}
|
|
|
|
report_missing_prerequisites() {
|
|
echo "WASM browser smoke prerequisites are missing: ${missing[*]}." >&2
|
|
echo "Run ./build-wasm.sh after activating emsdk to generate web/public/cnc_sim.js and web/public/cnc_sim.wasm." >&2
|
|
}
|
|
|
|
if [[ -z "$chromium_bin" ]]; then
|
|
missing+=("chromium")
|
|
elif [[ ! -x "$chromium_bin" ]]; then
|
|
missing+=("executable chromium at $chromium_bin")
|
|
fi
|
|
required_files=(
|
|
web/public/cnc_sim.js
|
|
web/public/cnc_sim.wasm
|
|
web/public/linuxcnc_switchkins_remap_config_cases.json
|
|
web/src/wasm-core.js
|
|
web/src/app.js
|
|
web/index.html
|
|
web/styles.css
|
|
web/test-browser-wasm-smoke.html
|
|
web/test-browser-wasm-smoke-helpers.js
|
|
web/test-browser-wasm-smoke-sections.js
|
|
web/test-browser-wasm-smoke-linuxcnc-sections.js
|
|
web/test-browser-wasm-smoke-opfs-workspace-sections.js
|
|
web/test-browser-wasm-smoke-opfs-basic-sections.js
|
|
web/test-browser-wasm-smoke-opfs-parameter-sections.js
|
|
web/test-browser-wasm-smoke-opfs-mirror-sections.js
|
|
web/test-browser-wasm-smoke-opfs-directory-sections.js
|
|
web/test-browser-wasm-smoke-opfs-policy-sections.js
|
|
web/test-browser-wasm-smoke-app-sections.js
|
|
)
|
|
|
|
for required in \
|
|
node \
|
|
python3 \
|
|
"${required_files[@]}"
|
|
do
|
|
if [[ "$required" == node || "$required" == python3 ]]; then
|
|
require_command "$required"
|
|
else
|
|
require_file "$required"
|
|
fi
|
|
done
|
|
|
|
if ((${#missing[@]} > 0)); then
|
|
report_missing_prerequisites
|
|
exit 1
|
|
fi
|
|
|
|
if ! python3 - <<'PY'
|
|
from pathlib import Path
|
|
import json
|
|
import sys
|
|
|
|
cases = json.loads(Path("web/public/linuxcnc_switchkins_remap_config_cases.json").read_text(encoding="utf-8"))
|
|
if not isinstance(cases, list) or not cases:
|
|
print("expected non-empty LinuxCNC switchkins config case JSON", file=sys.stderr)
|
|
sys.exit(1)
|
|
for index, config_case in enumerate(cases):
|
|
if (
|
|
not isinstance(config_case, dict)
|
|
or not isinstance(config_case.get("field"), str)
|
|
or not isinstance(config_case.get("value"), str)
|
|
or not isinstance(config_case.get("m428"), int)
|
|
or not isinstance(config_case.get("m429"), int)
|
|
or not isinstance(config_case.get("m430"), int)
|
|
):
|
|
print(f"invalid LinuxCNC switchkins config case at index {index}", file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
echo "WASM browser smoke switchkins config case JSON check failed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
html = Path("web/test-browser-wasm-smoke.html").read_text(encoding="utf-8")
|
|
if '<script src="/public/cnc_sim.js"></script>' not in html:
|
|
print("browser smoke HTML must load /public/cnc_sim.js", file=sys.stderr)
|
|
sys.exit(1)
|
|
module_scripts = re.findall(r'<script\s+type="module">(.*?)</script>', html, flags=re.S)
|
|
if len(module_scripts) != 1 or not module_scripts[0].strip():
|
|
print("browser smoke HTML must contain exactly one non-empty module script", file=sys.stderr)
|
|
sys.exit(1)
|
|
if 'from "/src/wasm-core.js"' not in module_scripts[0]:
|
|
print("browser smoke module script must import /src/wasm-core.js", file=sys.stderr)
|
|
sys.exit(1)
|
|
if 'from "/test-browser-wasm-smoke-sections.js"' not in module_scripts[0]:
|
|
print("browser smoke module script must import /test-browser-wasm-smoke-sections.js", file=sys.stderr)
|
|
sys.exit(1)
|
|
section_entry = Path("web/test-browser-wasm-smoke-sections.js").read_text(encoding="utf-8")
|
|
for imported_module in (
|
|
"/test-browser-wasm-smoke-helpers.js",
|
|
"/test-browser-wasm-smoke-linuxcnc-sections.js",
|
|
"/test-browser-wasm-smoke-opfs-workspace-sections.js",
|
|
"/test-browser-wasm-smoke-opfs-policy-sections.js",
|
|
"/test-browser-wasm-smoke-app-sections.js",
|
|
):
|
|
if f'from "{imported_module}"' not in section_entry:
|
|
print(f"browser smoke sections module must import {imported_module}", file=sys.stderr)
|
|
sys.exit(1)
|
|
opfs_workspace = Path("web/test-browser-wasm-smoke-opfs-workspace-sections.js").read_text(encoding="utf-8")
|
|
for imported_module in (
|
|
"/test-browser-wasm-smoke-opfs-basic-sections.js",
|
|
"/test-browser-wasm-smoke-opfs-parameter-sections.js",
|
|
"/test-browser-wasm-smoke-opfs-mirror-sections.js",
|
|
"/test-browser-wasm-smoke-opfs-directory-sections.js",
|
|
):
|
|
if f'from "{imported_module}"' not in opfs_workspace:
|
|
print(f"browser smoke OPFS workspace module must import {imported_module}", file=sys.stderr)
|
|
sys.exit(1)
|
|
app_html = Path("web/index.html").read_text(encoding="utf-8")
|
|
if '<link rel="stylesheet" href="/styles.css" />' not in app_html:
|
|
print("browser app HTML must load /styles.css", file=sys.stderr)
|
|
sys.exit(1)
|
|
if '<script type="module" src="/src/app.js"></script>' not in app_html:
|
|
print("browser app HTML must load /src/app.js", file=sys.stderr)
|
|
sys.exit(1)
|
|
for required_id in ("parseBtn", "programInput", "alarmList", "axisX", "wasmState", "storageState"):
|
|
if f'id="{required_id}"' not in app_html:
|
|
print(f"browser app HTML missing #{required_id}", file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
exit 1
|
|
fi
|
|
|
|
for web_module in \
|
|
web/src/wasm-core.js \
|
|
web/src/app.js \
|
|
web/test-browser-wasm-smoke-helpers.js \
|
|
web/test-browser-wasm-smoke-sections.js \
|
|
web/test-browser-wasm-smoke-linuxcnc-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-workspace-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-basic-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-parameter-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-mirror-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-directory-sections.js \
|
|
web/test-browser-wasm-smoke-opfs-policy-sections.js \
|
|
web/test-browser-wasm-smoke-app-sections.js
|
|
do
|
|
if ! node --check "$web_module"; then
|
|
echo "WASM browser smoke web module syntax check failed for $web_module" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
chromium_version=$("$chromium_bin" --version 2>/dev/null || true)
|
|
if [[ -n "$chromium_version" ]]; then
|
|
echo "browser smoke chromium version: $chromium_version"
|
|
fi
|
|
virtual_time_budget=${BROWSER_SMOKE_VIRTUAL_TIME_BUDGET:-20000}
|
|
|
|
server_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_server.XXXXXX.log")
|
|
dom_log=$(mktemp "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_dom.XXXXXX.html")
|
|
profile_dir=$(mktemp -d "${TMPDIR:-/tmp}/cnc_sim_browser_smoke_profile.XXXXXX")
|
|
cleanup() {
|
|
if [[ -n "${chromium_pid:-}" ]]; then
|
|
kill "$chromium_pid" >/dev/null 2>&1 || true
|
|
wait "$chromium_pid" >/dev/null 2>&1 || true
|
|
fi
|
|
if [[ -n "${server_pid:-}" ]]; then
|
|
kill "$server_pid" >/dev/null 2>&1 || true
|
|
wait "$server_pid" >/dev/null 2>&1 || true
|
|
fi
|
|
rm -f "$server_log" "$dom_log"
|
|
rm -rf "$profile_dir"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
pick_free_port() {
|
|
python3 - <<'PY'
|
|
import socket
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
print(sock.getsockname()[1])
|
|
PY
|
|
}
|
|
|
|
print_browser_failure_context() {
|
|
if grep -F "browser wasm smoke failed:" "$dom_log" >/dev/null; then
|
|
grep -o "browser wasm smoke failed:[^<]*" "$dom_log" | sed -n '1,20p' >&2
|
|
elif grep -F "running:" "$dom_log" >/dev/null; then
|
|
grep -o "running:[^<]*" "$dom_log" | tail -n 1 >&2
|
|
elif [[ ! -s "$dom_log" ]]; then
|
|
echo "browser smoke did not produce DOM output" >&2
|
|
fi
|
|
echo "browser smoke HTTP server log:" >&2
|
|
sed -n '1,40p' "$server_log" >&2
|
|
echo "browser smoke DOM excerpt:" >&2
|
|
sed -n '1,120p' "$dom_log" >&2
|
|
}
|
|
|
|
probe_http_resource() {
|
|
local resource_path=$1
|
|
local expected_prefix=$2
|
|
local expected_type=${3:-}
|
|
if ! python3 - "$port" "$resource_path" "$expected_prefix" "$expected_type" <<'PY'
|
|
import http.client
|
|
import sys
|
|
|
|
port = int(sys.argv[1])
|
|
path = sys.argv[2]
|
|
expected_prefix = sys.argv[3].encode("utf-8")
|
|
expected_type = sys.argv[4]
|
|
|
|
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=2)
|
|
try:
|
|
conn.request("GET", path)
|
|
response = conn.getresponse()
|
|
body = response.read(max(len(expected_prefix), 1))
|
|
finally:
|
|
conn.close()
|
|
|
|
if response.status != 200:
|
|
print(f"{path} returned HTTP {response.status}", file=sys.stderr)
|
|
sys.exit(1)
|
|
content_type = response.getheader("Content-Type", "").split(";", 1)[0]
|
|
if expected_type and content_type != expected_type:
|
|
print(f"{path} returned Content-Type {content_type!r}, expected {expected_type!r}", file=sys.stderr)
|
|
sys.exit(1)
|
|
if not body:
|
|
print(f"{path} returned an empty body", file=sys.stderr)
|
|
sys.exit(1)
|
|
if expected_prefix and not body.startswith(expected_prefix):
|
|
print(f"{path} did not start with expected content", file=sys.stderr)
|
|
sys.exit(1)
|
|
PY
|
|
then
|
|
echo "browser smoke HTTP resource probe failed for $resource_path" >&2
|
|
sed -n '1,40p' "$server_log" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
port=$(pick_free_port)
|
|
|
|
echo "running WASM browser smoke with $chromium_bin on http://127.0.0.1:$port/test-browser-wasm-smoke.html"
|
|
python3 -m http.server "$port" --bind 127.0.0.1 --directory web >"$server_log" 2>&1 &
|
|
server_pid=$!
|
|
|
|
for _ in {1..50}; do
|
|
if ! kill -0 "$server_pid" >/dev/null 2>&1; then
|
|
echo "browser smoke HTTP server exited early" >&2
|
|
sed -n '1,40p' "$server_log" >&2
|
|
exit 1
|
|
fi
|
|
if python3 - "$port" <<'PY'
|
|
import socket
|
|
import sys
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.settimeout(0.1)
|
|
sys.exit(0 if sock.connect_ex(("127.0.0.1", int(sys.argv[1]))) == 0 else 1)
|
|
PY
|
|
then
|
|
break
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
if ! python3 - "$port" <<'PY'
|
|
import socket
|
|
import sys
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
sock.settimeout(0.1)
|
|
sys.exit(0 if sock.connect_ex(("127.0.0.1", int(sys.argv[1]))) == 0 else 1)
|
|
PY
|
|
then
|
|
echo "browser smoke HTTP server did not start on port $port" >&2
|
|
sed -n '1,40p' "$server_log" >&2
|
|
exit 1
|
|
fi
|
|
|
|
probe_http_resource "/test-browser-wasm-smoke.html" "<!doctype html>" "text/html"
|
|
probe_http_resource "/" "<!doctype html>" "text/html"
|
|
probe_http_resource "/styles.css" "" "text/css"
|
|
probe_http_resource "/public/cnc_sim.js" "" "text/javascript"
|
|
probe_http_resource "/public/cnc_sim.wasm" "" "application/wasm"
|
|
probe_http_resource "/public/linuxcnc_switchkins_remap_config_cases.json" "[" "application/json"
|
|
probe_http_resource "/src/app.js" "" "text/javascript"
|
|
probe_http_resource "/src/wasm-core.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-helpers.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-linuxcnc-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-workspace-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-basic-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-parameter-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-mirror-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-directory-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-opfs-policy-sections.js" "" "text/javascript"
|
|
probe_http_resource "/test-browser-wasm-smoke-app-sections.js" "" "text/javascript"
|
|
|
|
debug_port=$(pick_free_port)
|
|
"$chromium_bin" \
|
|
--headless=new \
|
|
--disable-gpu \
|
|
--no-sandbox \
|
|
--user-data-dir="$profile_dir" \
|
|
--remote-debugging-port="$debug_port" \
|
|
"http://127.0.0.1:$port/test-browser-wasm-smoke.html" >"$dom_log" 2>&1 &
|
|
chromium_pid=$!
|
|
|
|
if ! python3 - "$debug_port" "$dom_log" "$virtual_time_budget" <<'PY'
|
|
import base64
|
|
import http.client
|
|
import json
|
|
import os
|
|
import socket
|
|
import struct
|
|
import sys
|
|
import time
|
|
from urllib.parse import urlparse
|
|
|
|
debug_port = int(sys.argv[1])
|
|
dom_log = sys.argv[2]
|
|
timeout_ms = int(sys.argv[3])
|
|
deadline = time.monotonic() + (timeout_ms / 1000.0)
|
|
|
|
def http_json(path):
|
|
conn = http.client.HTTPConnection("127.0.0.1", debug_port, timeout=1)
|
|
try:
|
|
conn.request("GET", path)
|
|
response = conn.getresponse()
|
|
body = response.read()
|
|
finally:
|
|
conn.close()
|
|
if response.status != 200:
|
|
raise RuntimeError(f"{path} returned HTTP {response.status}")
|
|
return json.loads(body.decode("utf-8"))
|
|
|
|
def websocket_connect(url):
|
|
parsed = urlparse(url)
|
|
key = base64.b64encode(os.urandom(16)).decode("ascii")
|
|
sock = socket.create_connection((parsed.hostname, parsed.port), timeout=2)
|
|
request = (
|
|
f"GET {parsed.path} HTTP/1.1\r\n"
|
|
f"Host: {parsed.hostname}:{parsed.port}\r\n"
|
|
"Upgrade: websocket\r\n"
|
|
"Connection: Upgrade\r\n"
|
|
f"Sec-WebSocket-Key: {key}\r\n"
|
|
"Sec-WebSocket-Version: 13\r\n\r\n"
|
|
)
|
|
sock.sendall(request.encode("ascii"))
|
|
response = sock.recv(4096)
|
|
if b" 101 " not in response.split(b"\r\n", 1)[0]:
|
|
raise RuntimeError("DevTools websocket handshake failed")
|
|
sock.settimeout(1)
|
|
return sock
|
|
|
|
def websocket_send(sock, payload):
|
|
data = payload.encode("utf-8")
|
|
header = bytearray([0x81])
|
|
if len(data) < 126:
|
|
header.append(0x80 | len(data))
|
|
elif len(data) < 65536:
|
|
header.append(0x80 | 126)
|
|
header.extend(struct.pack("!H", len(data)))
|
|
else:
|
|
header.append(0x80 | 127)
|
|
header.extend(struct.pack("!Q", len(data)))
|
|
mask = os.urandom(4)
|
|
header.extend(mask)
|
|
masked = bytes(byte ^ mask[index % 4] for index, byte in enumerate(data))
|
|
sock.sendall(header + masked)
|
|
|
|
def recv_exact(sock, size):
|
|
chunks = []
|
|
remaining = size
|
|
while remaining:
|
|
chunk = sock.recv(remaining)
|
|
if not chunk:
|
|
raise RuntimeError("DevTools websocket closed")
|
|
chunks.append(chunk)
|
|
remaining -= len(chunk)
|
|
return b"".join(chunks)
|
|
|
|
def websocket_recv(sock):
|
|
first, second = recv_exact(sock, 2)
|
|
opcode = first & 0x0F
|
|
length = second & 0x7F
|
|
if length == 126:
|
|
length = struct.unpack("!H", recv_exact(sock, 2))[0]
|
|
elif length == 127:
|
|
length = struct.unpack("!Q", recv_exact(sock, 8))[0]
|
|
masked = bool(second & 0x80)
|
|
mask = recv_exact(sock, 4) if masked else b""
|
|
payload = recv_exact(sock, length)
|
|
if masked:
|
|
payload = bytes(byte ^ mask[index % 4] for index, byte in enumerate(payload))
|
|
if opcode == 8:
|
|
raise RuntimeError("DevTools websocket closed")
|
|
if opcode != 1:
|
|
return None
|
|
return json.loads(payload.decode("utf-8"))
|
|
|
|
def find_page_ws_url():
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
for target in http_json("/json/list"):
|
|
if target.get("type") == "page" and target.get("url", "").endswith("/test-browser-wasm-smoke.html"):
|
|
return target["webSocketDebuggerUrl"]
|
|
except Exception:
|
|
pass
|
|
time.sleep(0.1)
|
|
raise RuntimeError("timed out waiting for browser DevTools page target")
|
|
|
|
sock = websocket_connect(find_page_ws_url())
|
|
next_id = 0
|
|
|
|
def cdp(method, params=None):
|
|
global next_id
|
|
next_id += 1
|
|
message_id = next_id
|
|
websocket_send(sock, json.dumps({"id": message_id, "method": method, "params": params or {}}))
|
|
while time.monotonic() < deadline:
|
|
try:
|
|
message = websocket_recv(sock)
|
|
except TimeoutError:
|
|
continue
|
|
except socket.timeout:
|
|
continue
|
|
if message and message.get("id") == message_id:
|
|
if "error" in message:
|
|
raise RuntimeError(message["error"])
|
|
return message["result"]
|
|
raise RuntimeError(f"timed out waiting for DevTools response to {method}")
|
|
|
|
def evaluate(expression):
|
|
result = cdp("Runtime.evaluate", {"expression": expression, "returnByValue": True})
|
|
return result.get("result", {}).get("value", "")
|
|
|
|
try:
|
|
cdp("Runtime.enable")
|
|
text = ""
|
|
while time.monotonic() < deadline:
|
|
text = evaluate('document.querySelector("#result")?.textContent || ""')
|
|
if "browser wasm smoke passed" in text:
|
|
html = evaluate("document.documentElement.outerHTML")
|
|
with open(dom_log, "w", encoding="utf-8") as handle:
|
|
handle.write(html)
|
|
sys.exit(0)
|
|
if "browser wasm smoke failed:" in text:
|
|
print(text, file=sys.stderr)
|
|
html = evaluate("document.documentElement.outerHTML")
|
|
with open(dom_log, "w", encoding="utf-8") as handle:
|
|
handle.write(html)
|
|
sys.exit(1)
|
|
time.sleep(0.2)
|
|
print(f"timed out waiting for browser smoke result; last status: {text}", file=sys.stderr)
|
|
html = evaluate("document.documentElement.outerHTML")
|
|
with open(dom_log, "w", encoding="utf-8") as handle:
|
|
handle.write(html)
|
|
sys.exit(1)
|
|
finally:
|
|
sock.close()
|
|
PY
|
|
then
|
|
echo "browser WASM smoke failed or timed out" >&2
|
|
print_browser_failure_context
|
|
exit 1
|
|
fi
|
|
|
|
if ! grep -F "browser wasm smoke passed" "$dom_log" >/dev/null; then
|
|
echo "browser WASM smoke did not pass" >&2
|
|
print_browser_failure_context
|
|
exit 1
|
|
fi
|
|
|
|
passed_text=$(grep -o "browser wasm smoke passed ([0-9][0-9]* sections)" "$dom_log" | tail -n 1 || true)
|
|
passed_sections=$(printf '%s\n' "$passed_text" | grep -o '[0-9][0-9]*' || true)
|
|
if [[ -z "$passed_sections" ]]; then
|
|
echo "browser WASM smoke did not report a completed section count" >&2
|
|
print_browser_failure_context
|
|
exit 1
|
|
fi
|
|
if ((passed_sections < minimum_sections)); then
|
|
echo "browser WASM smoke covered $passed_sections sections, expected at least $minimum_sections" >&2
|
|
print_browser_failure_context
|
|
exit 1
|
|
fi
|
|
echo "${passed_text:-browser wasm smoke passed}"
|