"""Focus ownership and supervisor failure-path regressions (no live desktop).""" from contextlib import ExitStack import ctypes from pathlib import Path import tempfile import unittest from unittest.mock import MagicMock, patch from ptlinux.presentation import game_window as gw class Clock: def __init__(self) -> None: self.now = 0.0 def sleep(self, seconds: float) -> None: self.now += seconds class WindowFocusRegressions(unittest.TestCase): def setUp(self) -> None: self.context = self.enterContext(ExitStack()) self.clock = Clock() self.context.enter_context(patch.object(gw.time, "sleep", self.clock.sleep)) self.context.enter_context(patch.object(gw.time, "monotonic", lambda: self.clock.now)) self.mapper = MagicMock(spec=gw._X11) self.mapper.window_state.return_value = 1 self.mapper.window_is_active.return_value = True self.mapper.window_is_focused.return_value = False self.mapper.activate_window.return_value = True def assert_no_input_change(self) -> None: self.mapper.focus_window.assert_not_called() self.mapper.restore_input_focus.assert_not_called() def test_denied_desktop_activation_is_not_overridden_with_raw_focus(self) -> None: self.mapper.window_is_active.return_value = False gw._focus_game_window(self.mapper, 12) self.assert_no_input_change() self.mapper.activate_window.assert_called_once_with(12) self.assertLessEqual(self.clock.now, 2.0) def test_slow_desktop_activation_is_confirmed_before_input_repair(self) -> None: self.mapper.window_is_active.side_effect = lambda _window: self.clock.now >= 0.6 self.mapper.window_is_focused.side_effect = [False, True] gw._focus_game_window(self.mapper, 12) self.assertGreaterEqual(self.clock.now, 0.6) self.assertLessEqual(self.clock.now, 2.0) self.mapper.restore_input_focus.assert_called_once_with(12) self.mapper.focus_window.assert_not_called() def test_already_focused_child_is_not_replaced_by_startup_handoff(self) -> None: self.mapper.window_is_focused.return_value = True gw._focus_game_window(self.mapper, 12) self.assert_no_input_change() self.mapper.activate_window.assert_not_called() def test_alt_tab_during_focus_inspection_is_not_undone(self) -> None: self.mapper.window_is_active.side_effect = [True, False] gw._repair_game_window_focus(self.mapper, 12) self.assert_no_input_change() def test_unmapped_or_unknown_window_is_never_focused(self) -> None: for state in (None, 0, 2, 3): for repair in (gw._repair_game_window_focus, gw._repair_gamescope_game_window_focus): with self.subTest(state=state, repair=repair.__name__): self.mapper.reset_mock() self.mapper.window_state.return_value = state with patch.object(gw, "_gamescope_host_focus_is_allowed", return_value=True): self.assertFalse(repair(self.mapper, 12)) self.assert_no_input_change() def test_nested_handoff_rechecks_the_host_after_the_dashboard_delay(self) -> None: with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": False}): gw._focus_game_window(self.mapper, 12, activate=False) self.assert_no_input_change() self.mapper.map_window.assert_not_called() def test_nested_startup_maps_the_ge_client_before_repairing_input(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) mapped_at = None def map_window(_window, *, borderless): nonlocal mapped_at self.assertFalse(borderless) mapped_at = self.clock.now self.mapper.map_window.side_effect = map_window self.mapper.window_state.side_effect = lambda _window: ( 1 if mapped_at is not None and self.clock.now - mapped_at >= 0.1 else None ) with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._prepare_gamescope_startup_window(self.mapper, 12, set()) gw._repair_gamescope_game_window_focus(self.mapper, 12) self.mapper.map_window.assert_called_once_with(12, borderless=False) self.mapper.restore_input_focus.assert_called_once_with(12) self.mapper.activate_window.assert_not_called() self.mapper.force_windowed_state.assert_not_called() self.mapper.refresh_render_surface.assert_not_called() def test_nested_startup_does_not_remap_existing_or_destroyed_windows(self) -> None: for state, size in ((1, (1920, 1080)), (3, (1920, 1080)), (0, (1920, 1080)), (None, None)): with self.subTest(state=state, size=size): self.mapper.reset_mock() self.mapper.window_state.return_value = state self.mapper.window_size.return_value = size with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._prepare_gamescope_startup_window(self.mapper, 12, set()) self.mapper.map_window.assert_not_called() self.mapper.activate_window.assert_not_called() def test_nested_startup_does_not_map_after_host_focus_changes(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) with patch.object(gw, "_gamescope_host_focus_status", side_effect=[ {"allowed": True}, {"allowed": False}, {"allowed": False}, ]): gw._prepare_gamescope_startup_window(self.mapper, 12, set()) self.mapper.map_window.assert_not_called() self.assert_no_input_change() def test_nested_startup_mapping_wait_is_bounded_and_does_not_force_focus(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._prepare_gamescope_startup_window(self.mapper, 12, set()) self.mapper.map_window.assert_called_once_with(12, borderless=False) self.assertLessEqual(self.clock.now, 1.0) self.assert_no_input_change() def test_nested_repair_rechecks_host_focus_before_setting_input(self) -> None: with patch.object(gw, "_gamescope_host_focus_status", side_effect=[ {"allowed": True}, {"allowed": False}, ]): gw._repair_gamescope_game_window_focus(self.mapper, 12) self.assert_no_input_change() def test_nested_mapping_is_deferred_until_host_focus_returns(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) self.mapper.find_first_window.return_value = None mapped_at = [] def map_window(_window, *, borderless): mapped_at.append(self.clock.now) self.mapper.window_state.return_value = 1 self.mapper.map_window.side_effect = map_window process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 2 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", side_effect=lambda **_kw: { "allowed": self.clock.now >= 1.0, }): gw._focus_game_window(self.mapper, 12, activate=False) gw._maintain_gamescope_game_window_focus(self.mapper, process, 12) self.assertEqual(len(mapped_at), 1) self.assertGreaterEqual(mapped_at[0], 1.0) self.mapper.activate_window.assert_not_called() self.mapper.restore_input_focus.assert_called_with(12) def test_nested_replacement_login_window_gets_its_own_startup_mapping(self) -> None: mapped = set() self.mapper.window_state.side_effect = lambda window: ( 1 if window in mapped or (window == 12 and self.clock.now < 0.5) else None ) self.mapper.window_size.side_effect = lambda window: ( (1920, 1080) if window == 21 else None ) self.mapper.find_first_window.return_value = (21, "PristonTale EU Login") self.mapper.map_window.side_effect = lambda window, **_kw: mapped.add(window) process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 2 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._maintain_gamescope_game_window_focus(self.mapper, process, 12) self.mapper.map_window.assert_called_once_with(21, borderless=False) self.mapper.restore_input_focus.assert_called_with(21) def test_nested_monitor_does_not_keep_remapping_an_unacknowledged_client(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) self.mapper.find_first_window.return_value = None process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 3 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._maintain_gamescope_game_window_focus(self.mapper, process, 12) self.mapper.map_window.assert_called_once_with(12, borderless=False) self.assert_no_input_change() def test_nested_mapping_stays_deferred_while_the_host_is_minimized(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) self.mapper.find_first_window.return_value = None process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 2 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": False}): gw._maintain_gamescope_game_window_focus(self.mapper, process, 12) self.mapper.map_window.assert_not_called() self.assert_no_input_change() def test_nested_monitor_does_not_resurrect_a_previously_managed_client(self) -> None: self.mapper.window_state.side_effect = lambda _window: 1 if self.clock.now < 0.5 else None self.mapper.window_size.return_value = (1920, 1080) self.mapper.find_first_window.return_value = None self.mapper.window_is_focused.return_value = True process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 2 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._maintain_gamescope_game_window_focus(self.mapper, process, 12) self.mapper.map_window.assert_not_called() self.assert_no_input_change() def test_nested_mapping_error_does_not_end_the_game_supervisor(self) -> None: self.mapper.window_state.return_value = None self.mapper.window_size.return_value = (1920, 1080) self.mapper.find_first_window.return_value = None self.mapper.map_window.side_effect = OSError("window disappeared") process = MagicMock() process.poll.side_effect = lambda: 0 if self.clock.now >= 2 else None process.wait.return_value = 0 with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): self.assertEqual(gw._maintain_gamescope_game_window_focus(self.mapper, process, 12), 0) self.mapper.map_window.assert_called_once_with(12, borderless=False) self.assert_no_input_change() def test_nested_startup_rechecks_minimize_before_mapping(self) -> None: self.mapper.window_state.side_effect = [None, 3] self.mapper.window_size.return_value = (1920, 1080) prepared_windows = set() with patch.object(gw, "_gamescope_host_focus_status", return_value={"allowed": True}): gw._prepare_gamescope_startup_window(self.mapper, 12, prepared_windows) # Losing WM_STATE after minimization must not trigger startup mapping. self.mapper.window_state.side_effect = None self.mapper.window_state.return_value = None gw._prepare_gamescope_startup_window(self.mapper, 12, prepared_windows) self.mapper.map_window.assert_not_called() self.assert_no_input_change() def test_activation_errors_do_not_fall_back_to_forced_focus(self) -> None: self.mapper.window_is_active.return_value = False self.mapper.activate_window.side_effect = RuntimeError("WM disconnected") gw._focus_game_window(self.mapper, 12) self.assert_no_input_change() def test_initial_preparation_respects_minimize_after_discovery(self) -> None: self.mapper.root_window_ids.return_value = set() self.mapper.find_first_window.return_value = (12, "PristonTale EU V23.86.96") self.mapper.window_state.return_value = 3 process = MagicMock() process.poll.return_value = None with ( patch.object(gw, "_X11", return_value=self.mapper), patch.object(gw.subprocess, "Popen", return_value=process), patch.object(gw, "WINDOW_READY_DELAY_SECONDS", 0), patch.object(gw, "_maintain_game_window_focus", return_value=17), ): self.assertEqual(gw.run_game_window(["unused"], borderless=True), 17) self.mapper.map_window.assert_not_called() self.mapper.force_windowed_state.assert_not_called() self.mapper.refresh_render_surface.assert_not_called() self.mapper.activate_window.assert_not_called() self.assert_no_input_change() def test_preparation_never_raises_a_client_before_desktop_activation(self) -> None: gw._prepare_game_window(self.mapper, 12, borderless=False, suppress_wm_ping=False, raise_window=False) self.mapper.map_window.assert_called_once_with(12, borderless=False, raise_window=False) self.assert_no_input_change() def test_layout_recovery_does_not_remap_or_raise_the_x11_client(self) -> None: mapper: gw._X11 = object.__new__(gw._X11) mapper.library = MagicMock() mapper.display = ctypes.c_void_p(1) with ( patch.object(mapper, "window_size", return_value=(1280, 720)), patch.object(mapper, "_remove_decorations"), ): self.assertEqual(mapper.map_window(12, borderless=True, raise_window=False), (1280, 720)) mapper.library.XMapRaised.assert_not_called() mapper.library.XMapWindow.assert_not_called() mapper.library.XSetInputFocus.assert_not_called() def test_bad_bridge_timestamps_do_not_crash_the_supervisor(self) -> None: with tempfile.TemporaryDirectory() as directory: state = Path(directory) / "focus" environment = {gw.GAMESCOPE_HOST_FOCUS_STATE_ENV: str(state)} for timestamp in ("nan", "inf", "-inf", "1e999", "1e308", "-1e308"): with self.subTest(timestamp=timestamp): state.write_text(f"1 {timestamp}\n", encoding="ascii") self.assertFalse(gw._gamescope_host_focus_is_allowed(environment=environment, now=10)) def test_startup_recovery_does_not_activate_a_background_window(self) -> None: self.mapper.window_is_active.return_value = False self.mapper.window_is_fullscreen_or_maximized.return_value = True process = MagicMock() process.poll.side_effect = [None, 0] with patch.object(gw, "_prepare_game_window") as prepare: gw._guard_game_window_startup(self.mapper, process, 12, borderless=False) prepare.assert_not_called() self.mapper.activate_window.assert_not_called() self.assert_no_input_change() def test_startup_recovery_does_not_undo_alt_tab_during_surface_refresh(self) -> None: self.mapper.window_is_fullscreen_or_maximized.return_value = True process = MagicMock() process.poll.side_effect = [None, 0] def switch_away(*_args, **_kwargs) -> None: self.mapper.window_is_active.return_value = False with patch.object(gw, "_prepare_game_window", side_effect=switch_away) as prepare: gw._guard_game_window_startup(self.mapper, process, 12, borderless=False) self.assertIs(prepare.call_args.kwargs["raise_window"], False) self.mapper.activate_window.assert_not_called() self.assert_no_input_change() def test_scaling_failures_keep_the_focus_supervisor_running(self) -> None: for failure in ("subscribe", "configure", "poll", "repair"): with self.subTest(failure=failure): mapper = MagicMock(spec=gw._X11) mapper.enable_window_scaling.return_value = failure != "configure" if failure == "subscribe": mapper.select_scaling_hint_events.side_effect = RuntimeError("subscription failed") elif failure == "poll": mapper.wait_for_scaling_hint_change.side_effect = RuntimeError("poll failed") elif failure == "repair": mapper.enable_window_scaling.side_effect = [True] + [RuntimeError("hints failed")] * 3 process = MagicMock() process.poll.return_value = None with patch.object(gw, "_maintain_game_window_focus", return_value=17) as maintain: self.assertEqual(gw._maintain_game_window_scaling(mapper, process, 12), 17) maintain.assert_called_once_with( mapper, process, 12, window_title=gw.WINDOW_TITLE, existing_window_ids=(), expected_pid=None, ) process.wait.assert_not_called() def test_delayed_desktop_activation_is_repaired_during_startup_guard(self) -> None: self.mapper.window_is_fullscreen_or_maximized.return_value = False self.mapper.window_is_active.side_effect = lambda _window: self.clock.now >= 1.5 process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 4 else 0 repair_times: list[float] = [] def repair(_window: int) -> None: repair_times.append(self.clock.now) self.mapper.window_is_focused.return_value = True self.mapper.restore_input_focus.side_effect = repair gw._guard_game_window_startup(self.mapper, process, 12, borderless=False) self.assertEqual(len(repair_times), 1) self.assertGreaterEqual(repair_times[0], 1.5) self.assertLess(repair_times[0], 4) self.mapper.activate_window.assert_not_called() self.mapper.map_window.assert_not_called() self.mapper.refresh_render_surface.assert_not_called() def test_startup_guard_hands_a_vanished_window_to_the_monitor_immediately(self) -> None: process = MagicMock() process.poll.return_value = None for state in (None, 0): with self.subTest(state=state): self.mapper.window_state.return_value = state self.assertFalse(gw._guard_game_window_startup( self.mapper, process, 12, borderless=False, )) self.assertEqual(self.clock.now, 0.0) self.assert_no_input_change() def test_direct_monitor_follows_the_same_process_without_resizing_or_activating(self) -> None: self.mapper.window_pid.return_value = 42 self.mapper.window_state.side_effect = lambda window: 0 if window == 12 else 1 self.mapper.find_first_window.return_value = (22, "PristonTale EU V23.86.96") self.mapper.restore_input_focus.side_effect = lambda _window: setattr( self.mapper.window_is_focused, "return_value", True, ) process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 3 else 17 process.wait.return_value = 17 self.assertEqual(gw._maintain_game_window_focus( self.mapper, process, 12, expected_pid=42, existing_window_ids={8, 9}, ), 17) self.mapper.find_first_window.assert_called_once_with( gw.WINDOW_TITLE, excluded_window_ids={8, 9, 12}, expected_pid=42, ) self.mapper.restore_input_focus.assert_called_once_with(22) self.mapper.activate_window.assert_not_called() self.mapper.map_window.assert_not_called() self.mapper.refresh_render_surface.assert_not_called() def test_a_replaced_background_window_does_not_take_keyboard_focus(self) -> None: self.mapper.window_pid.return_value = 42 self.mapper.window_state.side_effect = lambda window: None if window == 12 else 1 self.mapper.find_first_window.return_value = (22, "PristonTale EU V23.86.96") self.mapper.window_is_active.return_value = False process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 2 else 0 process.wait.return_value = 0 gw._maintain_game_window_focus(self.mapper, process, 12, expected_pid=42) self.assert_no_input_change() self.mapper.activate_window.assert_not_called() def test_minimized_direct_client_is_not_replaced_or_restored(self) -> None: self.mapper.window_pid.return_value = 42 self.mapper.window_state.return_value = 3 process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 2 else 0 process.wait.return_value = 0 gw._maintain_game_window_focus(self.mapper, process, 12, expected_pid=42) self.mapper.find_first_window.assert_not_called() self.mapper.map_window.assert_not_called() self.mapper.restore_normal_window_state.assert_not_called() self.assert_no_input_change() def test_missing_process_identity_does_not_attach_to_another_account(self) -> None: self.mapper.window_pid.return_value = None self.mapper.window_state.return_value = None process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 2 else 0 process.wait.return_value = 0 gw._maintain_game_window_focus(self.mapper, process, 12) self.mapper.find_first_window.assert_not_called() self.assert_no_input_change() def test_reused_window_id_from_another_process_is_not_focused(self) -> None: self.mapper.window_pid.return_value = 99 self.mapper.find_first_window.return_value = None process = MagicMock() process.poll.side_effect = lambda: None if self.clock.now < 2 else 0 process.wait.return_value = 0 gw._maintain_game_window_focus(self.mapper, process, 12, expected_pid=42) self.assert_no_input_change() for call in self.mapper.find_first_window.call_args_list: self.assertEqual(call.kwargs["expected_pid"], 42) self.assertIn(12, call.kwargs["excluded_window_ids"]) def test_discovery_keeps_the_original_pid_if_the_window_disappears_during_startup(self) -> None: self.mapper.root_window_ids.return_value = {8, 9} self.mapper.find_first_window.return_value = (12, "PristonTale EU V23.86.96") self.mapper.window_pid.return_value = 42 process = MagicMock() process.poll.return_value = None def disappear(*_args, **_kwargs) -> bool: self.mapper.window_pid.return_value = None return False with ( patch.object(gw, "_X11", return_value=self.mapper), patch.object(gw.subprocess, "Popen", return_value=process), patch.object(gw, "WINDOW_READY_DELAY_SECONDS", 0), patch.object(gw, "_prepare_game_window"), patch.object(gw, "_focus_game_window"), patch.object(gw, "_guard_game_window_startup", side_effect=disappear), patch.object(gw, "_maintain_game_window_focus", return_value=17) as maintain, ): self.assertEqual(gw.run_game_window(["unused"]), 17) maintain.assert_called_once_with( self.mapper, process, 12, window_title=gw.WINDOW_TITLE, existing_window_ids={8, 9}, expected_pid=42, ) class ClientWindowDiscoveryTests(unittest.TestCase): def test_replacement_discovery_rejects_other_processes_with_identical_titles(self) -> None: mapper: gw._X11 = object.__new__(gw._X11) with ( patch.object(mapper, "_root_windows", return_value=[50, 60, 70]), patch.object(mapper, "_client_window", side_effect={50: 12, 60: 13, 70: 14}.__getitem__), patch.object(mapper, "window_pid", side_effect={12: 99, 13: None, 14: 42}.__getitem__), patch.object(mapper, "_window_title", return_value="PristonTale EU V23.86.96"), ): self.assertEqual(mapper.find_first_window( gw.WINDOW_TITLE, excluded_window_ids=set(), expected_pid=42, ), (14, "PristonTale EU V23.86.96")) def test_title_is_read_from_the_client_inside_an_untitled_wm_frame(self) -> None: mapper: gw._X11 = object.__new__(gw._X11) with ( patch.object(mapper, "_root_windows", return_value=[50]), patch.object(mapper, "_client_window", return_value=12), patch.object(mapper, "_window_title", side_effect=lambda window: "PristonTale EU V23.86.96" if window == 12 else None), ): self.assertEqual(mapper.find_first_window("PristonTale EU", excluded_window_ids=set()), (12, "PristonTale EU V23.86.96")) def test_an_existing_client_is_excluded_even_if_its_frame_changes(self) -> None: mapper: gw._X11 = object.__new__(gw._X11) frames = {50: 12, 60: 12, 70: 13} with ( patch.object(mapper, "_root_windows", side_effect=[[50], [60, 70]]), patch.object(mapper, "_client_window", side_effect=frames.__getitem__), patch.object(mapper, "_window_title", return_value="PristonTale EU V23.86.96"), ): existing = mapper.root_window_ids() found = mapper.find_first_window("PristonTale EU", excluded_window_ids=existing) self.assertIn(12, existing) self.assertEqual(found, (13, "PristonTale EU V23.86.96")) def test_a_retired_client_is_excluded_even_when_a_frame_supplies_its_title(self) -> None: mapper: gw._X11 = object.__new__(gw._X11) with ( patch.object(mapper, "_root_windows", return_value=[50]), patch.object(mapper, "_client_window", return_value=12), patch.object(mapper, "_window_title", return_value="PristonTale EU V23.86.96"), ): self.assertIsNone(mapper.find_first_window("PristonTale EU", excluded_window_ids={12}))