from dataclasses import asdict, replace import ctypes import json from pathlib import Path import subprocess import tempfile import unittest from unittest.mock import MagicMock, patch from ptlinux.application.commands.play import ( ensure_portable_umu_for_play, play_locked, runtime_issue_summary, ) from ptlinux.cli import _load_or_default_settings from ptlinux.depot import DepotError, DepotLaunchCatalog, DepotStartOption from ptlinux.game.arguments import split_windows_arguments from ptlinux.game.depot.catalogs import select_depot_start_option from ptlinux.game.depot.manifest import parse_start_options from ptlinux.game.runtime import ( RuntimeErrorPT, RuntimeSelection, game_runtime_ready, prepare_game_runtime, run_game, run_runtime_prefix_command, runtime_settings_for_realm, select_runtime, ) from ptlinux.game.runtime_view import ( KNOWN_SOURCE_SHA256, OverlayError, _scene_action, prepare_runtime_view, select_scene_action, ) from ptlinux.paths import AppPaths from ptlinux.presentation import game_window from ptlinux.settings import ( Settings, ensure_settings, load_settings, reset_settings, save_settings, update_settings, ) from ptlinux.telemetry.runtime import _selected_prefix, _wine_command class WindowsArgumentTests(unittest.TestCase): def test_preserves_windows_paths_quotes_empty_arguments_and_shell_literals(self) -> None: arguments = ( "PTEU_REBORN", r"C:\game\data", "Babel's Realm", "", "a b", 'a"b', "trailing\\", r"$(touch /tmp/should-not-exist)", "a;b", "%TEMP%", ) self.assertEqual(split_windows_arguments(subprocess.list2cmdline(arguments)), arguments) def test_windows_quoting_examples(self) -> None: for command, expected in ( ('"a b c" d e', ("a b c", "d", "e")), (r'a\\\b d"e f"g h', (r"a\\\b", "de fg", "h")), (r'a\\\"b c d', ('a\\"b', "c", "d")), (r'a\\\\"b c" d e', (r"a\\b c", "d", "e")), ('a"b"" c d', ('ab" c d',)), ("Babel's Realm", ("Babel's", "Realm")), (" \t ", ()), ): with self.subTest(command=command): self.assertEqual(split_windows_arguments(command), expected) def test_manifest_allows_no_arguments_and_keeps_publisher_default(self) -> None: options, default = parse_start_options( [ {"Name": "Babel", "Path": "Game.exe", "Arguments": ""}, {"Name": "New Realm", "Path": "New.exe"}, ], "New Realm", ) self.assertEqual(options[0].arguments, "") self.assertEqual(options[1].arguments, "") catalog = DepotLaunchCatalog(10, options, default) self.assertEqual(select_depot_start_option(catalog, "Removed Realm"), options[1]) class RuntimeDefaultMigrationTests(unittest.TestCase): def setUp(self) -> None: temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name) self.paths = AppPaths(*(self.root / name for name in ("data", "config", "state", "cache"))) self.paths.config.mkdir(parents=True) def write_settings(self, value: dict[str, object]) -> None: self.paths.settings_file.write_text(json.dumps(value), encoding="utf-8") def test_new_and_reset_installations_use_ge_proton(self) -> None: self.assertEqual(Settings().runtime, "ge-proton") self.assertEqual(load_settings(self.paths).runtime, "ge-proton") self.assertFalse(self.paths.settings_file.exists()) self.assertEqual(ensure_settings(self.paths).runtime, "ge-proton") save_settings(self.paths, Settings(runtime="proton7")) self.assertEqual(reset_settings(self.paths).runtime, "ge-proton") self.assertEqual(ensure_settings(self.paths).runtime, "ge-proton") def test_all_old_profiles_migrate_without_changing_other_preferences(self) -> None: for runtime in ("auto", "proton7", "proton7-container", "umu", "ge-proton"): with self.subTest(runtime=runtime): old_settings = Settings( runtime=runtime, proton_path="/Steam Library/Proton 7.0/proton", renderer="wined3d", proton_sync="esync", telemetry_enabled=False, game_crash_dump_upload_enabled=False, game_source="/Games/Publisher", ui_scale="150", borderless=True, allow_window_resizing=True, suppress_unresponsive_warnings=False, download_speed_limit_mbps=4.0, low_bandwidth_mode=True, update_channel="prerelease", ) self.write_settings({ **asdict(old_settings), "schema": 1, "title_frame_default_migrated": True, "future_setting": {"keep": True}, }) before = self.paths.settings_file.read_bytes() expected = replace(old_settings, runtime="ge-proton") self.assertEqual(load_settings(self.paths), expected) self.assertEqual(self.paths.settings_file.read_bytes(), before) self.assertEqual(ensure_settings(self.paths), expected) payload = json.loads(self.paths.settings_file.read_text()) self.assertEqual(payload["runtime"], "ge-proton") self.assertIs(payload["ge_proton_default_migrated"], True) self.assertEqual(payload["future_setting"], {"keep": True}) self.assertEqual(payload["schema"], 1) self.assertEqual(self.paths.settings_file.stat().st_mode & 0o777, 0o600) def test_migration_is_persisted_once_and_later_manual_choices_survive(self) -> None: self.write_settings({"runtime": "proton7", "ge_proton_default_migrated": False}) self.assertEqual(ensure_settings(self.paths).runtime, "ge-proton") before = self.paths.settings_file.stat() self.assertEqual(ensure_settings(self.paths).runtime, "ge-proton") after = self.paths.settings_file.stat() self.assertEqual((after.st_ino, after.st_mtime_ns), (before.st_ino, before.st_mtime_ns)) for runtime in ("auto", "proton7", "proton7-container", "umu"): with self.subTest(runtime=runtime): update_settings(self.paths, runtime=runtime) update_settings(self.paths, proton_sync="esync") self.assertEqual(ensure_settings(self.paths).runtime, runtime) self.assertEqual(load_settings(self.paths).runtime, runtime) def test_unrelated_update_migrates_the_runtime(self) -> None: self.write_settings({"runtime": "umu"}) settings = update_settings(self.paths, low_bandwidth_mode=True) self.assertEqual(settings.runtime, "ge-proton") self.assertTrue(settings.low_bandwidth_mode) self.assertEqual(ensure_settings(self.paths), settings) def test_explicit_runtime_choice_during_first_save_is_preserved(self) -> None: for full_save in (False, True): with self.subTest(full_save=full_save): self.write_settings({"runtime": "auto"}) if full_save: save_settings(self.paths, Settings(runtime="umu")) else: update_settings(self.paths, runtime="umu") self.assertEqual(ensure_settings(self.paths).runtime, "umu") def test_migration_keeps_the_ptr_override_and_ignores_its_path_for_babel(self) -> None: proton = self.root / "Steam/steamapps/common/Proton 7.0/proton" proton.parent.mkdir(parents=True) proton.touch() for realm in ("PTR Babel", "Test Babel"): with self.subTest(realm=realm): self.write_settings({"runtime": "proton7", "realm": realm, "proton_path": str(proton)}) settings = ensure_settings(self.paths) self.assertEqual(settings.runtime, "ge-proton") self.assertEqual(settings.realm, realm) self.assertEqual(runtime_settings_for_realm(settings).runtime, "proton7") with patch("ptlinux.game.runtime.find_umu_launcher", return_value="/usr/bin/umu-run"): ptr = select_runtime(settings, {"HOME": str(self.root)}) babel = select_runtime(replace(settings, realm="Babel"), {}) self.assertEqual(ptr.kind, "proton7") self.assertEqual(ptr.proton_path, str(proton)) self.assertEqual(babel.kind, "ge-proton") self.assertIsNone(babel.proton_path) def test_invalid_runtime_and_marker_are_rejected_without_rewriting(self) -> None: for value in ( {"runtime": "unsupported"}, {"runtime": None}, {"runtime": True}, {"ge_proton_default_migrated": "true"}, {"ge_proton_default_migrated": 1}, ): with self.subTest(value=value): self.write_settings(value) before = self.paths.settings_file.read_bytes() with self.assertRaises(ValueError): ensure_settings(self.paths) self.assertEqual(self.paths.settings_file.read_bytes(), before) def test_default_cannot_silently_fall_back_to_legacy_proton(self) -> None: with ( patch("ptlinux.game.runtime.find_umu_launcher", return_value=None), patch("ptlinux.game.runtime.find_steam_proton7") as find_legacy, ): with self.assertRaisesRegex(RuntimeErrorPT, "umu-run was not found"): select_runtime(Settings(), {}) find_legacy.assert_not_called() def test_default_bootstrap_error_does_not_fall_back_to_proton7(self) -> None: api = MagicMock() api.provision_umu.side_effect = RuntimeError("network unavailable") with self.assertRaisesRegex(RuntimeError, "portable UMU.*network unavailable"): ensure_portable_umu_for_play(api, self.paths, Settings(), dry_run=False) api.select_runtime.assert_not_called() def test_cli_migrates_existing_settings_and_dry_runs_remain_read_only(self) -> None: self.write_settings({"runtime": "proton7"}) before = self.paths.settings_file.read_bytes() self.assertEqual(_load_or_default_settings(self.paths, persist=False).runtime, "ge-proton") self.assertEqual(self.paths.settings_file.read_bytes(), before) self.assertEqual(_load_or_default_settings(self.paths, persist=True).runtime, "ge-proton") payload = json.loads(self.paths.settings_file.read_text()) self.assertEqual(payload["runtime"], "ge-proton") self.assertIs(payload["ge_proton_default_migrated"], True) class LaunchCompatibilityTests(unittest.TestCase): def setUp(self) -> None: self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.paths = AppPaths(*(self.root / name for name in ("data", "config", "state", "cache"))) self.paths.runtime_view.mkdir(parents=True) (self.paths.runtime_view / "Game.exe").touch() self.runner = MagicMock(dry_run=False) self.selection = RuntimeSelection("ge-proton", "/usr/bin/umu-run", None, None) self.host_check = patch("ptlinux.game.runtime._require_container_host") self.host_check.start() self.addCleanup(self.host_check.stop) def test_recovers_legacy_renderer_option_and_persists_migration(self) -> None: self.paths.config.mkdir(parents=True) self.paths.settings_file.write_text(json.dumps({"proton_path": "PROTON_USE_WINED3D=1"})) settings = ensure_settings(self.paths) self.assertIsNone(settings.proton_path) self.assertEqual(settings.renderer, "wined3d") self.assertEqual(load_settings(self.paths), settings) self.assertIsNone(json.loads(self.paths.settings_file.read_text())["proton_path"]) with patch("ptlinux.game.runtime.find_umu_launcher", return_value="/usr/bin/umu-run"): selection = select_runtime(settings, {}) run_game(self.paths, settings, selection, self.runner, use_gamebox=False) environment = self.runner.run.call_args.kwargs["env_overrides"] self.assertEqual(environment["PROTON_USE_WINED3D"], "1") self.assertIn("d3d9=b", environment["WINEDLLOVERRIDES"]) def test_does_not_execute_or_migrate_arbitrary_path_commands(self) -> None: for value in ("PROTON_LOG=1", "env PROTON_USE_WINED3D=1 %command%"): with self.subTest(value=value): settings = Settings(runtime="auto", proton_path=value) with self.assertRaisesRegex(ValueError, "filesystem path"): save_settings(self.paths, settings) with self.assertRaisesRegex(RuntimeErrorPT, "filesystem path"): select_runtime(settings, {}) def test_existing_explicit_renderer_wins_over_misplaced_legacy_option(self) -> None: self.paths.config.mkdir(parents=True) self.paths.settings_file.write_text(json.dumps({ "proton_path": "PROTON_USE_WINED3D=1", "renderer": "dxvk", })) self.assertEqual(ensure_settings(self.paths).renderer, "dxvk") def test_proton_script_field_accepts_a_steam_proton_directory_with_spaces(self) -> None: proton = self.root / "Steam/steamapps/common/Proton 7.0/proton" proton.parent.mkdir(parents=True) proton.touch() selection = select_runtime(Settings(runtime="proton7", proton_path=str(proton.parent)), {}) self.assertEqual(selection.proton_path, str(proton)) def test_auto_selects_ge_across_distributions_and_keeps_explicit_umu(self) -> None: with patch("ptlinux.game.runtime.find_umu_launcher", return_value="/usr/bin/umu-run"): for runtime in ("auto", "ge-proton"): for distribution in ("ubuntu", "cachyos", "arch", "fedora"): with self.subTest(runtime=runtime, distribution=distribution): self.assertEqual(select_runtime(Settings(runtime=runtime), {}, distribution_id=distribution).kind, "ge-proton") self.assertEqual(select_runtime(Settings(runtime="umu"), {}).kind, "umu") def test_future_publisher_scene_is_copied_unchanged_without_old_texture_repair(self) -> None: source = self.paths.source_game for relative, content in ( ("Game.exe", b"new executable"), ("game.dll", b"new dll"), ("game.ini", b"[Screen]\nWindowed=1\n"), ("game/maps/WhisperingVale.rovug", b"future publisher scene format"), ): path = source / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(content) with patch("ptlinux.game.runtime_view.normalize_wrapped_assbin_textures") as repair: result = prepare_runtime_view(self.paths, source) self.assertEqual(result.scene_action, "upstream-unmodified-copy") self.assertEqual(result.binary_action, "upstream-pair") self.assertEqual(result.source_scene_sha256, result.runtime_scene_sha256) self.assertEqual((source / "game/maps/WhisperingVale.rovug").read_bytes(), b"future publisher scene format") repair.assert_not_called() def test_unknown_scene_texture_count_does_not_trigger_legacy_patch(self) -> None: self.assertEqual(_scene_action(MagicMock(file_sha256="new", embedded_textures=())), "upstream-unmodified-copy") self.assertEqual(select_scene_action("new", [16, 16], [2, 2]), "upstream-unmodified-copy") self.assertEqual(select_scene_action(KNOWN_SOURCE_SHA256, [16, 16], [2, 2]), "normalize-known-profile") with self.assertRaises(OverlayError): select_scene_action(KNOWN_SOURCE_SHA256, [16, 8], [2, 2]) def test_ge_setup_game_and_helpers_use_one_isolated_wow64_prefix(self) -> None: self.runner.dry_run = True prepare_game_runtime(self.paths, self.selection, self.runner) run_game(self.paths, Settings(), self.selection, self.runner, use_gamebox=False) run_runtime_prefix_command(self.paths, self.selection, self.runner, ["reg.exe", "query"]) self.assertEqual(self.runner.run.call_count, 3) for call in self.runner.run.call_args_list: environment = call.kwargs["env_overrides"] self.assertEqual(environment["PROTONPATH"], "GE-Proton") self.assertEqual(environment["PROTON_USE_WOW64"], "1") self.assertEqual(environment["WINEPREFIX"], str(self.paths.ge_proton_prefix)) self.assertNotEqual(self.paths.ge_proton_prefix, self.paths.umu_prefix) self.assertNotEqual(self.paths.ge_proton_prefix, self.paths.proton_prefix) def test_failed_directx_postcondition_does_not_mark_runtime_ready(self) -> None: with self.assertRaisesRegex(RuntimeErrorPT, "DirectX setup completed without"): prepare_game_runtime(self.paths, self.selection, self.runner) self.assertFalse(self.paths.game_runtime_marker.exists()) self.assertFalse(game_runtime_ready(self.paths, self.selection)) def test_shared_clients_cannot_reuse_a_prefix_for_another_runtime(self) -> None: self.paths.state.mkdir(parents=True) for prefix in (self.paths.umu_prefix, self.paths.ge_proton_prefix): directory = prefix / "drive_c/windows/syswow64" directory.mkdir(parents=True) for name in ("d3dx9_43", "d3dcompiler_43"): (directory / f"{name}.dll").touch() (prefix / "user.reg").write_text('"*d3dx9_43"="native"\n"*d3dcompiler_43"="native"\n') self.paths.game_runtime_marker.write_text(json.dumps({ "configuration_version": 1, "runtime": asdict(self.selection), })) self.assertTrue(game_runtime_ready(self.paths, self.selection)) self.assertFalse(game_runtime_ready(self.paths, RuntimeSelection("umu", "/usr/bin/umu-run", None, None))) def test_shared_ge_clients_keep_same_environment_except_for_verb(self) -> None: for concurrent in (False, True): run_game( self.paths, Settings(), self.selection, self.runner, use_gamebox=False, concurrent_prefix_launch=concurrent, ) first, second = [dict(call.kwargs["env_overrides"]) for call in self.runner.run.call_args_list] self.assertEqual(first.pop("PROTON_VERB"), "waitforexitandrun") self.assertEqual(second.pop("PROTON_VERB"), "runinprefix") self.assertEqual(first, second) def test_renderer_is_environment_and_native_helpers_preserve_unrelated_overrides(self) -> None: for kind in ("umu", "ge-proton", "proton7", "proton7-container"): for renderer, value, override in (("dxvk", "0", "d3d9=n,b"), ("wined3d", "1", "d3d9=b")): with self.subTest(kind=kind, renderer=renderer): selection = RuntimeSelection(kind, "/usr/bin/umu-run", "/Steam/proton", "/Steam") run_game( self.paths, Settings(renderer=renderer), selection, self.runner, use_gamebox=False, extra_environment={"WINEDLLOVERRIDES": "*d3dx9_43,foo=b;d3dcompiler_43=;d3d9=b"}, ) environment = self.runner.run.call_args.kwargs["env_overrides"] self.assertEqual(environment["PROTON_USE_WINED3D"], value) self.assertEqual(environment["WINEDLLOVERRIDES"], f"foo=b;d3dx9_43=n,b;d3dcompiler_43=n,b;{override}") self.assertNotIn("PROTON_USE_WINED3D", " ".join(self.runner.run.call_args.args[0])) def test_windows_arguments_are_passed_in_order_without_shell_expansion(self) -> None: folder = self.paths.runtime_view / "New Client" folder.mkdir() (folder / "CLIENT.exe").touch() arguments = ("NEW_TOKEN", r"C:\data\assets", "", "a b", "$(echo literal)") option = DepotStartOption("New Realm", "New Client/client.exe", subprocess.list2cmdline(arguments)) run_game(self.paths, Settings(), self.selection, self.runner, start_option=option, use_gamebox=False) call = self.runner.run.call_args self.assertEqual(call.args[0], ["/usr/bin/umu-run", str(folder / "CLIENT.exe"), *arguments]) self.assertEqual(call.kwargs["cwd"], self.paths.runtime_view) def test_missing_advertised_executable_fails_before_runtime_start(self) -> None: with self.assertRaisesRegex(RuntimeErrorPT, "missing New.exe"): run_game(self.paths, Settings(), self.selection, self.runner, start_option=DepotStartOption("New", "New.exe", ""), use_gamebox=False) self.runner.run.assert_not_called() def test_build_mismatch_blocks_play_before_game_view_or_runtime_changes(self) -> None: self.paths.state.mkdir(parents=True) self.paths.native_depot_state.write_text(json.dumps({"build": 100, "dry_run": False})) api = MagicMock() api.current_telemetry.return_value = None api.game_import_requires_verification.return_value = False api._load_or_default_settings.return_value = Settings() api.is_running_inside_steam_runtime.return_value = False for build in (99, 101, None): api.read_cached_depot_launch_catalog.return_value = ( DepotLaunchCatalog(build, (DepotStartOption("Babel", "Game.exe", "NEW"),), "Babel") if build is not None else None ) with self.subTest(build=build), self.assertRaisesRegex(DepotError, "update.*before playing"): play_locked(api, self.paths, None, False, False, False, False) api.prepare_runtime_view.assert_not_called() api.prepare_game_runtime.assert_not_called() api.run_game.assert_not_called() def test_proton_telemetry_does_not_report_host_wine_as_bundled_wine(self) -> None: with patch("ptlinux.telemetry.runtime.shutil.which", return_value="/usr/bin/wine") as which: for kind in ("umu", "ge-proton"): self.assertIsNone(_wine_command({"kind": kind, "executable": "/usr/bin/umu-run"}, {})) which.assert_not_called() (self.paths.ge_proton_prefix / "drive_c/windows").mkdir(parents=True) self.assertEqual(_selected_prefix(self.paths, {"runtime": asdict(self.selection)}), self.paths.ge_proton_prefix) def test_gstreamer_negotiation_errors_are_visible_without_claiming_crash_cause(self) -> None: report = runtime_issue_summary( "GStreamer-Video-CRITICAL: gst_video_info_from_caps: assertion 'gst_caps_is_fixed (caps)' failed\n" "GStreamer-Audio-CRITICAL: gst_audio_info_from_caps: assertion 'gst_caps_is_fixed (caps)' failed\n" ) self.assertEqual(report["gstreamer_caps_error_count"], 2) self.assertFalse(report["gstreamer_stream_error"]) class GameKeyboardFocusTests(unittest.TestCase): def test_startup_hands_keyboard_focus_to_the_desktop_game_window(self) -> None: # Model a WM that rejects raw X11 focus changes to an inactive client. mapper = MagicMock(spec=game_window._X11) mapper.window_state.return_value = 1 active, keyboard = [7], [7] pending: list[int] = [] def activate(window: int) -> bool: pending.append(window) return True def settle(_seconds: float) -> None: if pending: active[0] = pending.pop() def focus(window: int) -> None: if active[0] == window: keyboard[0] = window mapper.activate_window.side_effect = activate mapper.window_is_active.side_effect = lambda window: active[0] == window mapper.window_is_focused.side_effect = lambda window: keyboard[0] == window mapper.restore_input_focus.side_effect = focus with patch.object(game_window.time, "sleep", side_effect=settle): game_window._focus_game_window(mapper, 12) self.assertEqual((active[0], keyboard[0]), (12, 12)) def test_activation_preserves_minimize_before_and_during_handoff(self) -> None: for states, requests in (([3], 0), ([1, 3], 1)): with self.subTest(states=states): mapper = MagicMock(spec=game_window._X11) mapper.window_state.side_effect = states mapper.window_is_active.return_value = False with patch.object(game_window.time, "sleep"): game_window._focus_game_window(mapper, 12) self.assertEqual(mapper.activate_window.call_count, requests) mapper.focus_window.assert_not_called() def test_nested_gamescope_focus_does_not_request_desktop_activation(self) -> None: mapper = MagicMock(spec=game_window._X11) mapper.window_state.return_value = 1 mapper.window_is_focused.return_value = False with patch.object(game_window.time, "sleep"): game_window._focus_game_window(mapper, 12, activate=False) mapper.activate_window.assert_not_called() mapper.restore_input_focus.assert_called_once_with(12) mapper.focus_window.assert_not_called() def test_activation_uses_only_the_games_own_user_time(self) -> None: for properties, expected in ( ({}, 0), ({(12, b"_NET_WM_USER_TIME"): (12345,)}, 12345), ({(12, b"_NET_WM_USER_TIME_WINDOW"): (13,), (13, b"_NET_WM_USER_TIME"): (45678,)}, 45678), ({(12, b"_NET_WM_USER_TIME_WINDOW"): (13,), (12, b"_NET_WM_USER_TIME"): (56789,)}, 56789), ): with self.subTest(properties=properties): mapper: game_window._X11 = object.__new__(game_window._X11) with ( patch.object(mapper, "_intern_atom", return_value=77), patch.object( mapper, "_long_property", side_effect=lambda window, name, _count: properties.get((window, name)), ), patch.object(mapper, "_send_root_client_message", return_value=True) as send, ): self.assertTrue(mapper.activate_window(12)) send.assert_called_once_with(12, 77, (1, expected, 0, 0, 0)) def test_focus_diagnostics_distinguish_a_request_from_actual_activation(self) -> None: mapper = MagicMock(spec=game_window._X11) mapper.window_state.return_value = 1 mapper.activate_window.return_value = True mapper.window_is_active.return_value = False mapper.window_diagnostic_snapshot.return_value = {"active": False, "focused": False} diagnostics = MagicMock() with ( patch.object(game_window.time, "sleep"), patch.object(game_window, "WINDOW_STATE_CONFIRM_TIMEOUT_SECONDS", 0.0), ): game_window._focus_game_window(mapper, 12, diagnostics=diagnostics) diagnostics.record.assert_called_once_with( "focus_handoff", window_id=12, activation_requested=True, activation_submitted=True, outcome="not_activated", active=False, focused=False, ) def test_focus_repair_respects_background_and_minimized_windows(self) -> None: for state, active in ((1, False), (3, True)): with self.subTest(state=state, active=active): mapper = MagicMock(spec=game_window._X11) mapper.window_state.return_value = state mapper.window_is_active.return_value = active mapper.window_is_focused.return_value = False self.assertFalse(game_window._repair_game_window_focus(mapper, 12)) mapper.restore_input_focus.assert_not_called() mapper.activate_window.assert_not_called() mapper.focus_window.assert_not_called() def test_borderless_launch_keeps_focus_repair_without_live_resizing(self) -> None: mapper = MagicMock(spec=game_window._X11) mapper.root_window_ids.return_value = set() mapper.find_first_window.return_value = (12, "PristonTale EU V23.86.96") process = MagicMock() process.poll.return_value = None with ( patch.object(game_window, "_X11", return_value=mapper), patch.object(game_window.subprocess, "Popen", return_value=process), patch.object(game_window, "WINDOW_READY_DELAY_SECONDS", 0), patch.object(game_window, "_prepare_game_window"), patch.object(game_window, "_focus_game_window"), patch.object(game_window, "_guard_game_window_startup", return_value=True), patch.object(game_window, "_maintain_game_window_scaling") as resize, patch.object(game_window, "_maintain_game_window_focus", return_value=17) as maintain, ): self.assertEqual(game_window.run_game_window(["unused"], borderless=True), 17) maintain.assert_called_once_with( mapper, process, 12, window_title=game_window.WINDOW_TITLE, existing_window_ids=set(), expected_pid=mapper.window_pid.return_value, ) resize.assert_not_called() mapper.close.assert_called_once_with() def test_child_input_focus_is_preserved_and_foreign_focus_is_rejected(self) -> None: for focused, parents, expected in ( (12, {}, True), (14, {14: 13, 13: 12}, True), (24, {24: 23, 23: 1000, 1000: 0}, False), (24, {}, False), # The focused window vanished during the read. (24, {24: 25, 25: 24}, False), (0, {}, False), (1, {}, False), ): with self.subTest(focused=focused, parents=parents): mapper: game_window._X11 = object.__new__(game_window._X11) mapper.display = ctypes.c_void_p(1) mapper.library = MagicMock() allocations: list[object] = [] def read_focus(_display, pointer, _revert): pointer._obj.value = focused return 1 def query_tree(_display, window, root, parent, children, count): if window not in parents: return 0 root._obj.value = 1000 parent._obj.value = parents[window] allocation = (ctypes.c_ulong * 1)(99) allocations.append(allocation) ctypes.cast(children, ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)))[0] = allocation count._obj.value = 1 return 1 mapper.library.XGetInputFocus.side_effect = read_focus mapper.library.XQueryTree.side_effect = query_tree with ( patch.object(mapper, "window_state", return_value=1), patch.object(mapper, "window_is_active", return_value=True), patch.object(mapper, "restore_input_focus") as restore, ): self.assertEqual(game_window._repair_game_window_focus(mapper, 12), expected) self.assertEqual(restore.call_count, 0 if expected else 1) self.assertEqual(mapper.library.XFree.call_count, len(allocations))