提示词:快速推进,避免纯 source-map-only,不自动推送。 结论:基于 LinuxCNC rs274ngc_pre.cc save_parameters() 的 filename + .new rename 语义,补强浏览器 OPFS moveFile 在新 WASM 实例中持久删除源文件并恢复目标 mirror 的覆盖;./test-native.sh 和 ./test-linuxcnc-source-link.sh 通过。
406 lines
14 KiB
Bash
Executable File
406 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")"
|
|
|
|
# LinuxCNC source basis: browser smoke verifies OPFS-backed wasm/API/app flows
|
|
# against source-backed LinuxCNC RS274 parameter persistence and generated
|
|
# switchkins/remap fixtures without bypassing OPFS.
|
|
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
|
|
cdp_driver=web/test-browser-wasm-smoke-cdp.py
|
|
browser_smoke_modules=(
|
|
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
|
|
)
|
|
required_files=(
|
|
web/public/cnc_sim.js
|
|
web/public/cnc_sim.wasm
|
|
web/public/linuxcnc_switchkins_remap_config_cases.json
|
|
web/index.html
|
|
web/styles.css
|
|
web/test-browser-wasm-smoke.html
|
|
"$cdp_driver"
|
|
"${browser_smoke_modules[@]}"
|
|
)
|
|
|
|
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)
|
|
required_browser_sections = {
|
|
"web/test-browser-wasm-smoke-linuxcnc-sections.js": (
|
|
'runSection("linuxcnc browser basic parse"',
|
|
'runSection("linuxcnc generated switchkins cases load"',
|
|
'runSection("linuxcnc browser parse parameter scratch cleanup"',
|
|
),
|
|
"web/test-browser-wasm-smoke-opfs-basic-sections.js": (
|
|
'runSection("opfs program parse and restore"',
|
|
),
|
|
"web/test-browser-wasm-smoke-opfs-parameter-sections.js": (
|
|
'runSection("opfs parameter restore in new instance"',
|
|
'runSection("opfs default parameter backup"',
|
|
'runSection("opfs invalid parameter restore failures"',
|
|
'runSection("opfs stale parameter state cleanup"',
|
|
),
|
|
"web/test-browser-wasm-smoke-opfs-directory-sections.js": (
|
|
'runSection("opfs directory persist and load"',
|
|
'runSection("opfs metadata queries avoid wasm mirror pollution"',
|
|
),
|
|
"web/test-browser-wasm-smoke-opfs-mirror-sections.js": (
|
|
'runSection("opfs copy and move mirror files"',
|
|
),
|
|
"web/test-browser-wasm-smoke-opfs-policy-sections.js": (
|
|
'runSection("browser opfs policy"',
|
|
),
|
|
"web/test-browser-wasm-smoke-app-sections.js": (
|
|
'runSection("browser app opfs save"',
|
|
'runSection("browser app opfs restore"',
|
|
),
|
|
}
|
|
for section_module, required_sections in required_browser_sections.items():
|
|
section_text = Path(section_module).read_text(encoding="utf-8")
|
|
for required_section in required_sections:
|
|
if required_section not in section_text:
|
|
print(f"browser smoke module {section_module} missing required section {required_section}", 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 "${browser_smoke_modules[@]}"; 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")
|
|
remove_profile_dir() {
|
|
local attempt
|
|
for attempt in {1..20}; do
|
|
if rm -rf "$profile_dir"; then
|
|
return 0
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
rm -rf "$profile_dir" >/dev/null 2>&1 || true
|
|
}
|
|
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"
|
|
remove_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"
|
|
for web_module in "${browser_smoke_modules[@]}"; do
|
|
probe_http_resource "${web_module#web}" "" "text/javascript"
|
|
done
|
|
|
|
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 "$cdp_driver" "$debug_port" "$dom_log" "$virtual_time_budget"; 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}"
|