Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
514
tests/test_window_focus_regressions.py
Normal file
514
tests/test_window_focus_regressions.py
Normal file
@@ -0,0 +1,514 @@
|
||||
"""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}))
|
||||
Reference in New Issue
Block a user