"""Exercise version/startup diagnostics on a private D-Bus session and Wine prefix. Run under xvfb-run. No requests go to the active screen reader. """ import argparse import json import os from pathlib import Path import shutil import subprocess import sys import time from gi.repository import Gio, GLib from wine_test_environment import add_test_options, run_wine_test, wine_test_directory parser = argparse.ArgumentParser() parser.add_argument("--build-dir", type=Path, default=Path("build")) parser.add_argument("--session", action="store_true") add_test_options(parser) args = parser.parse_args() buildDir = args.build_dir.resolve() if not args.session: sys.exit(run_wine_test(["dbus-run-session", "--", sys.executable, __file__, "--session", "--build-dir", str(buildDir)], buildDir, "diagnostics_test", keep=args.keep, keepOnFailure=args.keepOnFailure)) busName = "org.stormux.Cthulhu1.Service" objectPath = "/org/stormux/Cthulhu1/Service" xml = '' for method, signature in (("PresentAccessibleEvent", "sssssssiis"), ("SpeakText", "sb"), ("CancelSpeech", "")): xml += '' xml += ''.join('' for kind in signature) xml += '' xml += '' def record(connection, sender, path, interface, method, parameters, invocation): invocation.return_value(GLib.Variant("(b)", (True,))) with wine_test_directory(prefix="wine-access-diagnostics-") as temporary: testDir = Path(temporary) env = os.environ.copy() env.update(WINEPREFIX=str(testDir / "prefix"), WINEDEBUG="-all", WINEDLLOVERRIDES="mscoree,mshtml=", CTHULHU_WINE_ACCESS_MANAGED="1") helper = None try: subprocess.run(["wineboot", "-u"], env=env, check=True, timeout=45) executable = buildDir / "wine-access.exe" version = subprocess.run([str(executable), "--version"], env=env, check=True, capture_output=True, text=True, timeout=5) assert version.stdout.startswith("Wine Access ") and not version.stderr # No reader exists yet; ordinary startup must identify itself and fail. failed = subprocess.run([str(executable), "--no-controller"], env=env, capture_output=True, text=True, timeout=5) assert failed.returncode != 0 and "reader service" in failed.stderr assert "wine-access: identity " in failed.stderr and "wine-access: ready " not in failed.stderr connection = Gio.bus_get_sync(Gio.BusType.SESSION, None) connection.register_object(objectPath, Gio.DBusNodeInfo.new_for_xml(xml).interfaces[0], record, None, None) connection.call_sync("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "RequestName", GLib.Variant("(su)", (busName, 0)), None, Gio.DBusCallFlags.NONE, 1000, None) for missingHooks in (False, True): selected = executable if missingHooks: incompleteDir = testDir / "incomplete" incompleteDir.mkdir() for name in ("wine-access.exe", "wine-access.exe.so"): shutil.copy2(buildDir / name, incompleteDir / name) selected = incompleteDir / "wine-access.exe" logPath = testDir / ("missing.log" if missingHooks else "healthy.log") with logPath.open("w") as diagnostics: helper = subprocess.Popen([str(selected), "--no-controller"], env=env, stdout=subprocess.DEVNULL, stderr=diagnostics) deadline = time.monotonic() + 5 while "wine-access: ready " not in logPath.read_text() and time.monotonic() < deadline: while GLib.MainContext.default().iteration(False): pass assert helper.poll() is None, logPath.read_text() time.sleep(0.01) lines = logPath.read_text().splitlines() identity = next(json.loads(line.removeprefix("wine-access: identity ")) for line in lines if line.startswith("wine-access: identity ")) ready = next(json.loads(line.removeprefix("wine-access: ready ")) for line in lines if line.startswith("wine-access: ready ")) assert identity["version"] in version.stdout and identity["revision"] in version.stdout for field in ("controls_enabled", "review", "msaa_worker", "win_events", "control_poll"): assert ready[field], (field, ready) assert not ready["controller"] for field in ("menu_hook", "input_hook", "host32_started"): assert ready[field] == (not missingHooks), (field, ready) if missingHooks: assert "native menu hook unavailable" in logPath.read_text() assert "32-bit menu host unavailable" in logPath.read_text() assert "unavailable (error 0)" not in logPath.read_text(), logPath.read_text() # A query must bypass the singleton held by this helper. queried = subprocess.run([str(selected), "--version"], env=env, check=True, capture_output=True, text=True, timeout=5) assert queried.stdout == version.stdout and not queried.stderr helper.terminate() helper.wait(timeout=5) helper = None finally: subprocess.run(["wineserver", "-k"], env=env, timeout=10, check=False) if helper is not None: helper.wait(timeout=10) print("PASS: version without reader/with singleton, unavailable reader, healthy and missing-hook startup")