Files
cthulhu/tests/native_browser.py

136 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Chromium AT-SPI proof on a private X server, bus, and browser profile."""
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
BROWSER_PROOFS = {
"fragments": ("./internal/review", "^TestNativeBrowserFragmentLines$", 30, 35, "browser-fragments.html"),
"positions": ("./internal/atspi", "^TestNativeBrowserSetPositions$", 25, 30, "browser-positions.html"),
"tabs": ("./internal/app", "^TestNativeBrowserTabs$", 30, 35),
"namedlinks": ("./internal/app", "^TestNativeBrowserNamedLinks$", 25, 30, "browser-named-links.html"),
"linkedheadings": ("./internal/app", "^TestNativeLinkedHeadings$", 45, 50, "browser-linked-headings.html"),
"catalog": ("./internal/atspi", "^TestNativeBrowserCatalogCapture$", 30, 35, "browser-catalog.html"),
"lines": ("./internal/atspi", "^TestNativeBrowserLogicalLines$", 25, 30, "browser-lines.html"),
"tables": ("./internal/app", "^TestNativeBrowserTables$", 35, 40),
"dynamic": ("./internal/app", "^TestNativeBrowserDynamicNavigation$", 40, 45),
"browser": ("./internal/atspi", "^TestNativeBrowser$", 45, 50),
"live": ("./internal/atspi", "^TestNativeBrowserLiveRegion$", 30, 35),
"document": ("./internal/app", "^TestNativeBrowserDocumentCommands$", 45, 50),
"editing": ("./internal/app", "^TestNativeBrowserEditing$", 45, 50),
"selection": ("./internal/app", "^TestNativeBrowserSelection$", 35, 40),
"modes": ("./internal/app", "^TestNativeBrowserModes$", 45, 50),
"keys": ("./internal/platform/xorg", "^TestNativeBrowseKeyTransitions$", 20, 25),
}
def selected_proofs():
selected = {name.strip() for name in os.environ.get(
"CTHULHU_NATIVE_BROWSER_TESTS", ",".join(BROWSER_PROOFS)).split(",") if name.strip()}
unknown = selected - BROWSER_PROOFS.keys()
if not selected or unknown:
raise ValueError("Select browser proofs from: " + ",".join(BROWSER_PROOFS) +
("; unknown: " + ",".join(sorted(unknown)) if unknown else ""))
return selected
def run_inside():
browser = os.environ.get("CTHULHU_BROWSER") or shutil.which("brave")
if not browser:
raise RuntimeError("Set CTHULHU_BROWSER to a Chromium-family executable")
selected = selected_proofs()
for name, proof in BROWSER_PROOFS.items():
if name in selected:
print(f"Running browser proof: {name}", flush=True)
# F7 toggles a profile-wide setting. Each proof must start from
# the same browser state whether run alone or after editing.
run_browser_proof(browser, proof)
def run_browser_proof(browser, proof):
with tempfile.TemporaryDirectory(prefix="cthulhu-browser-profile-") as profile:
with tempfile.TemporaryFile() as log:
process = subprocess.Popen([
browser, "--user-data-dir=" + profile, "--no-first-run",
"--no-default-browser-check", "--disable-background-networking",
"--disable-component-update", "--disable-sync", "--password-store=basic",
"--force-renderer-accessibility", "--ozone-platform=x11",
"--new-window", Path(__file__).with_name(proof[4] if len(proof) > 4 else "browser.html").resolve().as_uri(),
], stdout=log, stderr=log)
try:
deadline = time.monotonic() + 20
while time.monotonic() < deadline:
if process.poll() is not None:
raise RuntimeError("Browser exited before the probe")
window_name = "Cthulhu Linked Headings Probe" if proof[1] == "^TestNativeLinkedHeadings$" else "Cthulhu Browser Probe"
result = subprocess.run(
["xdotool", "search", "--onlyvisible", "--name", window_name],
capture_output=True, text=True)
if result.stdout.strip():
window = result.stdout.splitlines()[0]
subprocess.run(["xdotool", "windowfocus", window], check=True)
break
time.sleep(0.1)
else:
raise RuntimeError("Browser fixture window missing")
environment = dict(os.environ, CTHULHU_NATIVE_BROWSER="1")
if proof[1] == "^TestNativeLinkedHeadings$":
environment["CTHULHU_NATIVE_LINKED_HEADINGS"] = "1"
environment.setdefault("GOCACHE", "/tmp/cthulhu-cache")
package, expression, testTimeout, runTimeout = proof[:4]
subprocess.run(["go", "test", package, "-run", expression,
"-count=1", "-v", f"-timeout={testTimeout}s"],
env=environment, check=True, timeout=runTimeout,
cwd=Path(__file__).resolve().parents[1])
finally:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
def main():
selected = selected_proofs()
if len(sys.argv) == 2 and sys.argv[1] == "--inside":
run_inside()
return
with tempfile.TemporaryDirectory(prefix="cthulhu-browser-"):
read_fd, write_fd = os.pipe()
server = subprocess.Popen(
["Xvfb", "-displayfd", str(write_fd), "-screen", "0", "800x600x24", "-nolisten", "tcp"],
pass_fds=(write_fd,),
stderr=subprocess.DEVNULL,
)
os.close(write_fd)
try:
with os.fdopen(read_fd) as reader:
display = reader.readline().strip()
if not display:
raise RuntimeError("Xvfb failed to start")
environment = dict(os.environ, DISPLAY=":" + display, XDG_SESSION_TYPE="x11")
environment.pop("DBUS_SESSION_BUS_ADDRESS", None)
environment.pop("AT_SPI_BUS_ADDRESS", None)
environment.pop("WAYLAND_DISPLAY", None)
subprocess.run(
["dbus-run-session", "--", sys.executable, str(Path(__file__).resolve()), "--inside"],
env=environment,
check=True,
# Budget each isolated browser startup, bounded Go run and
# cleanup instead of imposing one 120-second suite limit.
timeout=15 + sum(30 + BROWSER_PROOFS[name][3] for name in selected),
)
finally:
server.terminate()
server.wait(timeout=3)
if __name__ == "__main__":
main()