#!/usr/bin/env python3 """Real Calc through the production reader, in a private X11/AT-SPI session.""" import os from pathlib import Path import shutil import signal import subprocess import sys import tempfile import time def wait_for(check, message, timeout=15): deadline = time.monotonic() + timeout while time.monotonic() < deadline: result = check() if result: return result time.sleep(0.05) raise RuntimeError(message) def run_inside(): import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi with tempfile.TemporaryDirectory(prefix="cthulhu-libreoffice-") as temporary: root = Path(temporary) profile = root / "profile" / "user" profile.mkdir(parents=True) (profile / "registrymodifications.xcu").write_text( '' '' 'false' 'false' '', encoding="utf-8", ) document = root / "Example.fods" shutil.copyfile(Path(__file__).with_name("libreoffice.fods"), document) environment = dict(os.environ, SAL_USE_VCLPLUGIN="gtk3", LC_ALL="C.UTF-8", GTK_MODULES="gail:atk-bridge", NO_AT_BRIDGE="0", XDG_CONFIG_HOME=str(root / "config"), XDG_CACHE_HOME=str(root / "cache"), XDG_DATA_HOME=str(root / "data")) application = subprocess.Popen( ["libreoffice", "-env:UserInstallation=" + (root / "profile").as_uri(), "--norestore", "--nodefault", "--nofirststartwizard", "--calc", str(document)], env=environment, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) try: def find_window(): result = subprocess.run( ["xdotool", "search", "--onlyvisible", "--class", "libreoffice-calc"], capture_output=True, text=True, check=False, ) return result.stdout.splitlines()[-1] if result.stdout.strip() else None window = wait_for(find_window, "Calc window missing") # Wait for the actual accessible sheet, without enumerating its # millions of cells. Dismiss disposable first-run UI if present. def ready(): pending = [Atspi.get_desktop(0)] remaining = 2500 found = False while pending and remaining: node = pending.pop() remaining -= 1 role = node.get_role_name() if role == "table": parent = node.get_parent() found |= parent is not None and parent.get_role_name() == "document spreadsheet" continue if role == "dialog" and node.get_name() == "Welcome to LibreOffice!": subprocess.run(["xdotool", "key", "Escape"], check=True) return False if role == "table cell": continue for index in range(min(node.get_child_count(), 200)): child = node.get_child_at_index(index) if child is not None: pending.append(child) return found wait_for(ready, "Calc accessible sheet missing") environment.update(CTHULHU_NATIVE_LIBREOFFICE="1", CTHULHU_CALC_WINDOW=window) environment.setdefault("GOCACHE", "/tmp/cthulhu-go-cache") subprocess.run( ["go", "test", "./internal/app", "-run", "^TestNativeLibreOfficeCalc$", "-count=1", "-timeout=50s", "-v"], cwd=Path(__file__).resolve().parents[1], env=environment, check=True, timeout=60, ) finally: if application.poll() is None: os.killpg(application.pid, signal.SIGTERM) application.wait(timeout=5) def main(): if sys.argv[1:] == ["--inside"]: run_inside() return with tempfile.TemporaryDirectory(prefix="cthulhu-libreoffice-session-") as temporary: environment = dict(os.environ, XDG_SESSION_TYPE="x11", XDG_CONFIG_HOME=temporary, XDG_CACHE_HOME=temporary, XDG_DATA_HOME=temporary) for name in ("DBUS_SESSION_BUS_ADDRESS", "AT_SPI_BUS_ADDRESS", "WAYLAND_DISPLAY"): environment.pop(name, None) subprocess.run( ["xvfb-run", "-a", "dbus-run-session", "--", sys.executable, str(Path(__file__).resolve()), "--inside"], env=environment, check=True, timeout=90, ) if __name__ == "__main__": main()