#!/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()