import contextlib import ctypes import io import os from pathlib import Path import tempfile import unittest from collections.abc import Collection from typing import Any from unittest import mock # noinspection PyProtectedMember from ptlinux.gamebox_runtime import ( GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS, GAMESCOPE_HOST_FOCUS_STATE_ENV, GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS, GameboxRuntimeError, WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS, WINDOW_ACTIVATION_DELAY_SECONDS, WINDOW_FOCUS_DELAY_SECONDS, WINDOW_GEOMETRY_CONFIRM_TIMEOUT_SECONDS, WINDOW_NUDGE_CONFIRM_TIMEOUT_SECONDS, WINDOW_SCALING_REPAIR_LIMIT, WINDOW_STARTUP_POLL_SECONDS, WINDOW_SURFACE_REFRESH_DELAY_SECONDS, WINDOW_SURFACE_SETTLE_SECONDS, WINDOW_TITLE, _X11, _X_PROPERTY_CHANGE_MASK, _X_PROPERTY_NOTIFY, _enable_game_window_scaling, _focus_game_window, _gamescope_host_focus_is_allowed, _guard_game_window_startup, _maintain_game_window_focus, _maintain_gamescope_game_window_focus, _maintain_game_window_scaling, _matches_game_window_title, _motif_hints_with_scaling, _normal_hints_without_fixed_size, _normal_hints_with_resize_increments, _process_exit_status, _prepare_game_window, _protocols_without, _repair_game_window_focus, _repair_gamescope_game_window_focus, _resolve_gamescope_game_window, _select_game_window, run_game_window, ) class GameboxRuntimeTests(unittest.TestCase): def test_maps_signals_before_returning_through_python(self) -> None: self.assertEqual(_process_exit_status(-9), 137) self.assertEqual(_process_exit_status(7), 7) def test_rejects_empty_game_command(self) -> None: with self.assertRaisesRegex(GameboxRuntimeError, "empty"): run_game_window([]) def test_game_title_match_accepts_ptr_prefix_and_rejects_dialogs(self) -> None: self.assertFalse( _matches_game_window_title( "Choose an existing PristonTale EU game directory", "PristonTale EU", ) ) self.assertFalse( _matches_game_window_title("Launching PristonTale EU", "PristonTale EU") ) self.assertTrue( _matches_game_window_title("PristonTale EU V21.75.83", "PristonTale EU") ) self.assertTrue( _matches_game_window_title( "PTR - PristonTale EU V21.78.89", "PristonTale EU", ) ) self.assertFalse( _matches_game_window_title( "PTR launcher for PristonTale EU", "PristonTale EU", ) ) def test_selects_only_a_new_game_window(self) -> None: windows = [ (10, "PristonTale EU V21.75.83"), (11, "Choose an existing PristonTale EU game directory"), (12, "PristonTale EU V21.75.83"), (13, "PristonTale EU V21.75.83"), (14, "PristonTale EU V21.75.83"), ] self.assertEqual( _select_game_window(windows, "PristonTale EU", {10, 12, 13}), (14, "PristonTale EU V21.75.83"), ) def test_resolves_a_window_manager_frame_to_its_client(self) -> None: mapper = object.__new__(_X11) children = { 20: [21, 22], 21: [], 22: [23], 23: [], } with ( mock.patch.object( mapper, "_has_wm_state", side_effect=lambda window: window == 23, ), mock.patch.object( mapper, "_child_windows", side_effect=lambda window: children[window], ), ): self.assertEqual(mapper._client_window(20), 23) def test_retains_the_frame_when_no_client_marker_exists(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object(mapper, "_has_wm_state", return_value=False), mock.patch.object(mapper, "_child_windows", return_value=[]), ): self.assertEqual(mapper._client_window(20), 20) def test_removes_only_wm_ping_from_window_protocols(self) -> None: self.assertEqual(_protocols_without((10, 20, 30, 20), 20), (10, 30)) def test_adds_scaling_without_changing_other_explicit_motif_hints(self) -> None: self.assertEqual( _motif_hints_with_scaling((0x03, 0x0C, 0x3A, 7, 8)), (0x03, 0x1E, 0x7E, 7, 8), ) def test_adds_scaling_to_exclusive_motif_hints(self) -> None: self.assertEqual( _motif_hints_with_scaling((0x03, 0x11, 0x41, 0, 0)), (0x03, 0x01, 0x01, 0, 0), ) def test_relaxes_only_an_exact_fixed_size_pair(self) -> None: hints = (0x30, 0, 0, 1600, 900, 1600, 900, 1600, 900, 1, 1) self.assertEqual( _normal_hints_without_fixed_size(hints, (1600, 900)), (0x00, *hints[1:]), ) self.assertEqual( _normal_hints_without_fixed_size(hints, (1280, 720)), (0x00, *hints[1:]), ) unrelated_range = (0x30, 0, 0, 800, 600, 640, 480, 1920, 1080) self.assertEqual( _normal_hints_without_fixed_size(unrelated_range, (1280, 720)), unrelated_range, ) def test_adds_resize_increments_without_losing_other_size_hints(self) -> None: hints = (0x01, 1, 2, 3, 4, 320, 240, 0, 0, 0, 0) updated = _normal_hints_with_resize_increments( hints, (1280, 960), (64, 48), ) self.assertEqual(updated[0], 0x141) self.assertEqual(updated[1:9], hints[1:9]) self.assertEqual(updated[9:11], (64, 48)) self.assertEqual(updated[15:17], (0, 0)) self.assertEqual(len(updated), 18) def test_keeps_matching_resize_increments_without_rewriting_x11_hints(self) -> None: hints = [0] * 18 hints[0] = 0x140 hints[9:11] = (64, 48) hints[15:17] = (0, 0) mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) with ( mock.patch.object(mapper, "_long_property", return_value=tuple(hints)), mock.patch.object(mapper, "_replace_long_property") as replace_property, ): self.assertTrue( mapper.set_resize_increments(12, (1280, 960), (64, 48)) ) replace_property.assert_not_called() mapper.library.XSync.assert_not_called() def test_caches_successful_x11_atom_lookups(self) -> None: class FakeX11Library: def __init__(self) -> None: self.calls: list[tuple[bytes, int]] = [] def XInternAtom( self, _display: object, name: bytes, only_if_exists: int, ) -> int: self.calls.append((name, only_if_exists)) return 41 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) self.assertEqual( mapper._intern_atom(b"WM_STATE", only_if_exists=True), 41, ) self.assertEqual( mapper._intern_atom(b"WM_STATE", only_if_exists=False), 41, ) self.assertEqual(library.calls, [(b"WM_STATE", 1)]) def test_retries_a_missing_x11_atom_lookup(self) -> None: class FakeX11Library: def __init__(self) -> None: self.calls: list[tuple[bytes, int]] = [] def XInternAtom( self, _display: object, name: bytes, only_if_exists: int, ) -> int: self.calls.append((name, only_if_exists)) return 0 if len(self.calls) == 1 else 77 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) self.assertEqual( mapper._intern_atom(b"_NET_ACTIVE_WINDOW", only_if_exists=True), 0, ) self.assertEqual( mapper._intern_atom(b"_NET_ACTIVE_WINDOW", only_if_exists=False), 77, ) self.assertEqual( mapper._intern_atom(b"_NET_ACTIVE_WINDOW", only_if_exists=True), 77, ) self.assertEqual( library.calls, [(b"_NET_ACTIVE_WINDOW", 1), (b"_NET_ACTIVE_WINDOW", 0)], ) def test_replaces_a_32_bit_window_property_without_losing_values(self) -> None: class FakeX11Library: def __init__(self) -> None: self.request: tuple[object, ...] | None = None def XInternAtom( self, _display: object, name: bytes, _only_if_exists: int, ) -> int: return { b"WM_NORMAL_HINTS": 31, b"WM_SIZE_HINTS": 32, }[name] def XChangeProperty( self, _display: object, window: int, atom: int, type_atom: int, format_bits: int, mode: int, data: Any, item_count: int, ) -> int: values = ctypes.cast(data, ctypes.POINTER(ctypes.c_ulong)) self.request = ( window, atom, type_atom, format_bits, mode, tuple(values[index] for index in range(item_count)), ) return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) mapper._replace_long_property( 12, b"WM_NORMAL_HINTS", b"WM_SIZE_HINTS", (0x10, 1600, 900), ) self.assertEqual( library.request, (12, 31, 32, 32, 0, (0x10, 1600, 900)), ) def test_requests_composited_presentation_for_a_host_window(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) with ( mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object(mapper, "_long_property", return_value=(2,)), ): self.assertTrue(mapper.prefer_composited_presentation(12)) replace_property.assert_not_called() mapper.library.XSync.assert_not_called() def test_updates_a_noncomposited_host_window_once(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) with ( mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object(mapper, "_long_property", side_effect=[(1,), (2,)]), ): self.assertTrue(mapper.prefer_composited_presentation(12)) replace_property.assert_called_once_with( 12, b"_NET_WM_BYPASS_COMPOSITOR", b"CARDINAL", (2,), ) mapper.library.XSync.assert_called_once_with(mapper.display, 0) def test_wm_ping_suppression_preserves_every_other_protocol(self) -> None: class FakeX11Library: def __init__(self) -> None: self.protocols = (10, 20, 30) self.buffers: list[ctypes.Array[ctypes.c_ulong]] = [] self.assert_atom_name = b"" def XInternAtom(self, _display: object, name: bytes, _only_if_exists: int) -> int: self.assert_atom_name = name return 20 def XGetWMProtocols( self, _display: object, _window: int, protocols_return: Any, count_return: Any, ) -> int: buffer = (ctypes.c_ulong * len(self.protocols))(*self.protocols) self.buffers.append(buffer) protocols_pointer = ctypes.cast( protocols_return, ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)), ) protocols_pointer[0] = ctypes.cast( buffer, ctypes.POINTER(ctypes.c_ulong), ) ctypes.cast( count_return, ctypes.POINTER(ctypes.c_int), )[0] = len(self.protocols) return 1 def XSetWMProtocols( self, _display: object, _window: int, protocols: Any, count: int, ) -> int: protocol_pointer = ctypes.cast( protocols, ctypes.POINTER(ctypes.c_ulong), ) self.protocols = tuple( int(protocol_pointer[index]) for index in range(count) ) return 1 def XSync(self, _display: object, _discard: int) -> int: return 0 def XFree(self, _value: object) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) self.assertTrue(mapper.suppress_wm_ping(12)) self.assertEqual(library.assert_atom_name, b"_NET_WM_PING") self.assertEqual(library.protocols, (10, 30)) def test_replaces_protons_generic_game_window_identity(self) -> None: class FakeX11Library: def __init__(self) -> None: self.identity: tuple[bytes, bytes] | None = None def XSetClassHint( self, _display: object, _window: int, hints_pointer: Any, ) -> int: hints = hints_pointer._obj self.identity = (hints.res_name, hints.res_class) return 1 def XSync(self, _display: object, _discard: int) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) mapper.set_window_identity(12) self.assertEqual(library.identity, (b"pristontale-eu", b"PristonTaleEU")) def test_reads_icccm_state_and_motif_decorations(self) -> None: class FakeX11Library: def __init__(self) -> None: self.buffers: list[ctypes.Array[ctypes.c_ulong]] = [] self.atoms = {b"WM_STATE": 10, b"_MOTIF_WM_HINTS": 20} self.properties = {10: (3, 0), 20: (2, 0, 0, 0, 0)} def XInternAtom( self, _display: object, name: bytes, _only_if_exists: int, ) -> int: return self.atoms.get(name, 0) def XGetWindowProperty( self, _display: object, _window: int, property_atom: int, _offset: int, _length: int, _delete: int, _requested_type: int, actual_type_return: Any, actual_format_return: Any, item_count_return: Any, bytes_after_return: Any, value_return: Any, ) -> int: values = self.properties[property_atom] buffer = (ctypes.c_ulong * len(values))(*values) self.buffers.append(buffer) ctypes.cast( actual_type_return, ctypes.POINTER(ctypes.c_ulong), )[0] = property_atom ctypes.cast( actual_format_return, ctypes.POINTER(ctypes.c_int), )[0] = 32 ctypes.cast( item_count_return, ctypes.POINTER(ctypes.c_ulong), )[0] = len(values) ctypes.cast( bytes_after_return, ctypes.POINTER(ctypes.c_ulong), )[0] = 0 value_pointer = ctypes.cast( value_return, ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), ) value_pointer[0] = ctypes.cast( buffer, ctypes.POINTER(ctypes.c_ubyte), ) return 0 def XFree(self, _value: object) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) self.assertEqual(mapper.window_state(12), 3) self.assertTrue(mapper.window_is_borderless(12)) library.properties[20] = (3, 0x3C, 0x7A, 0, 0) self.assertFalse(mapper.window_is_borderless(12)) def test_detects_the_desktop_active_window(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.library.XDefaultRootWindow.return_value = 99 mapper.display = ctypes.c_void_p(1) with mock.patch.object( mapper, "_long_property", return_value=(12,), ) as read_property: self.assertTrue(mapper.window_is_active(12)) read_property.assert_called_once_with(99, b"_NET_ACTIVE_WINDOW", 1) def test_detects_fullscreen_or_maximized_ewmh_state(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object( mapper, "_long_property", return_value=(17, 102), ), mock.patch.object( mapper, "_screen_filling_state_atoms", return_value=(101, 102, 103), ), ): self.assertTrue(mapper.window_is_fullscreen_or_maximized(12)) def test_force_windowed_state_removes_maximized_before_fullscreen(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object( mapper, "_long_property", return_value=(101, 102, 103), ), mock.patch.object( mapper, "_screen_filling_state_atoms", return_value=(101, 102, 103), ), mock.patch.object(mapper, "_intern_atom", return_value=100), mock.patch.object( mapper, "_send_root_client_message", return_value=True, ) as send_message, mock.patch.object( mapper, "_wait_for_windowed_state", return_value=True, ), ): self.assertTrue(mapper.force_windowed_state(12)) send_message.assert_has_calls( [ mock.call(12, 100, (0, 102, 103, 1, 0)), mock.call(12, 100, (0, 101, 0, 1, 0)), ] ) def test_force_windowed_state_rejects_an_unconfirmed_request(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object( mapper, "_long_property", return_value=(101,), ), mock.patch.object( mapper, "_screen_filling_state_atoms", return_value=(101, 102, 103), ), mock.patch.object(mapper, "_intern_atom", return_value=100), mock.patch.object( mapper, "_send_root_client_message", return_value=True, ), mock.patch.object( mapper, "_wait_for_windowed_state", return_value=False, ), self.assertRaisesRegex(GameboxRuntimeError, "did not confirm"), ): mapper.force_windowed_state(12) def test_enables_scaling_only_after_the_wm_confirms_all_actions(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) normal_hints = ( 0x30, 0, 0, 1600, 900, 1600, 900, 1600, 900, 1, 1, ) motif_hints = (0x03, 0x0C, 0x3A, 0, 0) with ( mock.patch.object(mapper, "window_allows_scaling", return_value=False), mock.patch.object(mapper, "window_size", return_value=(1600, 900)), mock.patch.object( mapper, "_long_property", side_effect=[normal_hints, motif_hints], ), mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object( mapper, "_wait_for_scaling_actions", return_value=True, ), ): self.assertTrue(mapper.enable_window_scaling(12)) replace_property.assert_has_calls( [ mock.call( 12, b"WM_NORMAL_HINTS", b"WM_SIZE_HINTS", (0x00, *normal_hints[1:]), ), mock.call( 12, b"_MOTIF_WM_HINTS", b"_MOTIF_WM_HINTS", (0x03, 0x1E, 0x7E, 0, 0), ), ] ) self.assertNotIn( b"_NET_WM_ALLOWED_ACTIONS", (call.args[1] for call in replace_property.call_args_list), ) def test_reapplies_scaling_hints_without_repeating_wm_confirmation(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) normal_hints = (0x30, 0, 0, 1600, 900, 1600, 900, 1600, 900) motif_hints = (0x03, 0x0C, 0x3A, 0, 0) with ( mock.patch.object(mapper, "window_size", return_value=(1600, 900)), mock.patch.object( mapper, "_long_property", side_effect=[normal_hints, motif_hints], ), mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object(mapper, "_wait_for_scaling_actions") as confirm_actions, ): self.assertTrue(mapper.enable_window_scaling(12, confirm=False)) self.assertEqual(replace_property.call_count, 2) confirm_actions.assert_not_called() mapper.library.XFlush.assert_called_once_with(mapper.display) mapper.library.XSync.assert_not_called() def test_restores_original_hints_when_scaling_is_not_confirmed(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) normal_hints = (0x20, 0, 0, 1600, 900, 0, 0, 1600, 900) motif_hints = (0x03, 0x0C, 0x3A, 0, 0) with ( mock.patch.object(mapper, "window_allows_scaling", return_value=False), mock.patch.object(mapper, "window_size", return_value=(1600, 900)), mock.patch.object( mapper, "_long_property", side_effect=[normal_hints, motif_hints], ), mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object( mapper, "_wait_for_scaling_actions", return_value=False, ), ): self.assertFalse(mapper.enable_window_scaling(12)) self.assertEqual(replace_property.call_count, 4) self.assertEqual( replace_property.call_args_list[-2:], [ mock.call( 12, b"_MOTIF_WM_HINTS", b"_MOTIF_WM_HINTS", motif_hints, ), mock.call( 12, b"WM_NORMAL_HINTS", b"WM_SIZE_HINTS", normal_hints, ), ] ) def test_waits_for_wm_actions_when_client_hints_are_already_resizable( self, ) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.display = ctypes.c_void_p(1) normal_hints = (0x04, 0, 0, 1280, 960, 0, 0, 0, 0) with ( mock.patch.object(mapper, "window_size", return_value=(1280, 960)), mock.patch.object( mapper, "_long_property", side_effect=[normal_hints, None], ), mock.patch.object(mapper, "_replace_long_property") as replace_property, mock.patch.object( mapper, "_wait_for_scaling_actions", return_value=True, ) as wait_for_actions, ): self.assertTrue(mapper.enable_window_scaling(12)) replace_property.assert_not_called() mapper.library.XSync.assert_called_once_with(mapper.display, 0) wait_for_actions.assert_called_once_with(12) def test_detects_native_resize_and_both_maximize_actions(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object(mapper, "_long_property", return_value=(7, 31, 32, 33)), mock.patch.object(mapper, "_intern_atom", side_effect=[31, 32, 33]), ): self.assertTrue(mapper.window_allows_scaling(12)) def test_window_diagnostic_snapshot_explains_missing_maximize_actions(self) -> None: mapper = object.__new__(_X11) normal_hints = [0] * 18 normal_hints[0] = (1 << 4) | (1 << 5) normal_hints[5:9] = [1280, 960, 1280, 960] def read_property( _window: int, name: bytes, _maximum_items: int, ) -> tuple[int, ...] | None: return { b"_NET_WM_STATE": (102,), b"_NET_WM_ALLOWED_ACTIONS": (31,), b"WM_NORMAL_HINTS": tuple(normal_hints), b"_MOTIF_WM_HINTS": (3, 2, 4, 0, 0), }.get(name) with ( mock.patch.object(mapper, "window_state", return_value=1), mock.patch.object(mapper, "window_size", return_value=(1280, 960)), mock.patch.object( mapper, "window_net_wm_state_flags", return_value={ "net_wm_state_present": True, "fullscreen": False, "maximized_horizontal": True, "maximized_vertical": False, "hidden": False, "above": False, "demands_attention": False, "skip_taskbar": False, }, ), mock.patch.object(mapper, "window_is_active", return_value=True), mock.patch.object(mapper, "window_is_focused", return_value=True), mock.patch.object(mapper, "window_is_borderless", return_value=False), mock.patch.object(mapper, "_long_property", side_effect=read_property), mock.patch.object( mapper, "_screen_filling_state_atoms", return_value=(101, 102, 103), ), mock.patch.object( mapper, "_intern_atom", side_effect=lambda name, **_kwargs: { b"_NET_WM_ACTION_RESIZE": 31, b"_NET_WM_ACTION_MAXIMIZE_HORZ": 32, b"_NET_WM_ACTION_MAXIMIZE_VERT": 33, b"_NET_WM_ACTION_MINIMIZE": 34, }[name], ), ): snapshot = mapper.window_diagnostic_snapshot(12) self.assertEqual(snapshot["wm_state"], "normal") self.assertTrue(snapshot["normal_fixed_size"]) self.assertTrue(snapshot["allows_resize"]) self.assertFalse(snapshot["allows_maximize_horizontal"]) self.assertFalse(snapshot["allows_maximize_vertical"]) self.assertTrue(snapshot["maximized_horizontal"]) self.assertFalse(snapshot["hidden"]) self.assertTrue(snapshot["motif_allows_resize"]) self.assertFalse(snapshot["motif_allows_maximize"]) def test_wait_for_scaling_actions_has_a_short_bounded_timeout(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object(mapper, "window_allows_scaling", return_value=False), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[10.0, 10.0 + WINDOW_ACTION_CONFIRM_TIMEOUT_SECONDS], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, ): self.assertFalse(mapper._wait_for_scaling_actions(12)) sleep.assert_not_called() def test_subscribes_only_to_property_changes_for_scaling_maintenance(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.library.XSelectInput.return_value = 1 mapper.display = ctypes.c_void_p(1) mapper.select_scaling_hint_events(12) mapper.library.XSelectInput.assert_called_once_with( mapper.display, 12, _X_PROPERTY_CHANGE_MASK, ) mapper.library.XSync.assert_called_once_with(mapper.display, 0) def test_wait_for_scaling_hint_change_uses_the_x11_event_socket(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.library.XPending.side_effect = [0, 1, 0] mapper.library.XConnectionNumber.return_value = 9 mapper.display = ctypes.c_void_p(1) def publish_property_event(_display: object, event_pointer: Any) -> int: event = event_pointer._obj event.property.type = _X_PROPERTY_NOTIFY event.property.window = 12 event.property.atom = 31 return 0 mapper.library.XNextEvent.side_effect = publish_property_event with ( mock.patch.object(mapper, "_intern_atom", side_effect=[31, 32, 33]), mock.patch( "ptlinux.gamebox_runtime.select.select", return_value=([9], [], []), ) as wait_for_event, ): self.assertTrue(mapper.wait_for_scaling_hint_change(12)) wait_for_event.assert_called_once() mapper.library.XNextEvent.assert_called_once() def test_wait_for_scaling_hint_change_times_out_without_polling_properties( self, ) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.library.XPending.return_value = 0 mapper.library.XConnectionNumber.return_value = 9 mapper.display = ctypes.c_void_p(1) with mock.patch( "ptlinux.gamebox_runtime.select.select", return_value=([], [], []), ): self.assertFalse(mapper.wait_for_scaling_hint_change(12)) mapper.library.XNextEvent.assert_not_called() def test_activate_window_sends_an_ewmh_restore_request(self) -> None: class FakeX11Library: def __init__(self) -> None: self.request: tuple[int, ...] | None = None def XInternAtom( self, _display: object, name: bytes, _only_if_exists: int, ) -> int: self.assert_name = name return 77 def XDefaultRootWindow(self, _display: object) -> int: return 99 def XSendEvent( self, _display: object, root: int, _propagate: int, mask: int, event_pointer: Any, ) -> int: event = event_pointer._obj.client_message self.request = ( root, mask, event.type, event.window, event.message_type, event.format, event.data.longs[0], event.data.longs[1], event.data.longs[2], event.data.longs[3], event.data.longs[4], ) return 1 def XSync(self, _display: object, _discard: int) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with mock.patch.object(mapper, "_long_property", return_value=None): self.assertTrue(mapper.activate_window(12)) self.assertEqual(library.assert_name, b"_NET_ACTIVE_WINDOW") self.assertEqual( library.request, (99, (1 << 19) | (1 << 20), 33, 12, 77, 32, 1, 0, 0, 0, 0), ) def test_borderless_mapping_captures_client_size_before_reframing(self) -> None: class FakeX11Library: def __init__(self) -> None: self.events: list[object] = [] def XMapRaised(self, _display: object, window: int) -> int: self.events.append(("map", window)) return 0 def XFlush(self, _display: object) -> int: self.events.append("flush") return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with ( mock.patch.object( mapper, "window_size", side_effect=lambda window: ( library.events.append(("size", window)) or (800, 450) ), ), mock.patch.object( mapper, "_remove_decorations", side_effect=lambda window: library.events.append( ("borderless", window) ), ), ): target_size = mapper.map_window(12, borderless=True) self.assertEqual(target_size, (800, 450)) self.assertEqual( library.events, [("size", 12), ("borderless", 12), ("map", 12), "flush"], ) def test_decorated_mapping_also_captures_the_client_size(self) -> None: class FakeX11Library: def __init__(self) -> None: self.events: list[object] = [] def XMapRaised(self, _display: object, window: int) -> int: self.events.append(("map", window)) return 0 def XFlush(self, _display: object) -> int: self.events.append("flush") return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with mock.patch.object( mapper, "window_size", side_effect=lambda window: ( library.events.append(("size", window)) or (1280, 960) ), ): target_size = mapper.map_window(12, borderless=False) self.assertEqual(target_size, (1280, 960)) self.assertEqual( library.events, [("size", 12), ("map", 12), "flush"], ) def test_wait_for_window_size_observes_delayed_wm_acknowledgement(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object( mapper, "window_size", side_effect=[(1280, 960), (1279, 960)], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, ): self.assertTrue(mapper._wait_for_window_size(12, (1279, 960))) sleep.assert_called_once() def test_wait_for_window_size_reports_a_wm_timeout(self) -> None: mapper = object.__new__(_X11) with ( mock.patch.object(mapper, "window_size", return_value=(1280, 960)), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[10.0, 10.5], ), ): self.assertFalse(mapper._wait_for_window_size(12, (1279, 960))) def test_render_surface_refresh_restores_a_pre_reframe_target_size(self) -> None: class FakeX11Library: def __init__(self) -> None: self.sizes: list[tuple[int, int]] = [] def XResizeWindow( self, _display: object, _window: int, width: int, height: int, ) -> int: self.sizes.append((width, height)) return 0 def XSync(self, _display: object, _discard: int) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with ( mock.patch.object(mapper, "window_size") as window_size, mock.patch.object( mapper, "_wait_for_window_size", return_value=True, ) as wait_for_size, mock.patch("ptlinux.gamebox_runtime.time.sleep"), ): self.assertTrue( mapper.refresh_render_surface(12, target_size=(800, 450)) ) window_size.assert_not_called() wait_for_size.assert_has_calls( [ mock.call( 12, (799, 450), timeout_seconds=WINDOW_NUDGE_CONFIRM_TIMEOUT_SECONDS, ), mock.call( 12, (800, 450), timeout_seconds=WINDOW_GEOMETRY_CONFIRM_TIMEOUT_SECONDS, ), ] ) self.assertEqual(library.sizes, [(799, 450), (800, 450)]) def test_render_surface_refresh_nudges_then_restores_the_mapped_window(self) -> None: class FakeX11Library: def __init__(self) -> None: self.events: list[object] = [] def XGetGeometry( self, _display: object, window: int, _root_return: Any, _x_return: Any, _y_return: Any, width_return: Any, height_return: Any, _border_width_return: Any, _depth_return: Any, ) -> int: self.events.append(("geometry", window)) ctypes.cast(width_return, ctypes.POINTER(ctypes.c_uint))[0] = 1600 ctypes.cast(height_return, ctypes.POINTER(ctypes.c_uint))[0] = 900 return 1 def XSync(self, _display: object, discard: int) -> int: self.events.append(("sync", discard)) return 0 def XResizeWindow( self, _display: object, window: int, width: int, height: int, ) -> int: self.events.append(("resize", window, width, height)) return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with ( mock.patch.object( mapper, "_wait_for_window_size", return_value=True, ) as wait_for_size, mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, ): self.assertTrue(mapper.refresh_render_surface(12)) sleep.assert_called_once_with(WINDOW_SURFACE_REFRESH_DELAY_SECONDS) wait_for_size.assert_has_calls( [ mock.call( 12, (1599, 900), timeout_seconds=WINDOW_NUDGE_CONFIRM_TIMEOUT_SECONDS, ), mock.call( 12, (1600, 900), timeout_seconds=WINDOW_GEOMETRY_CONFIRM_TIMEOUT_SECONDS, ), ] ) self.assertEqual( library.events, [ ("geometry", 12), ("resize", 12, 1599, 900), ("sync", 0), ("resize", 12, 1600, 900), ("sync", 0), ], ) def test_render_surface_refresh_restores_size_after_an_interrupted_wait(self) -> None: class FakeX11Library: def __init__(self) -> None: self.events: list[object] = [] def XGetGeometry( self, _display: object, _window: int, _root_return: Any, _x_return: Any, _y_return: Any, width_return: Any, height_return: Any, _border_width_return: Any, _depth_return: Any, ) -> int: ctypes.cast(width_return, ctypes.POINTER(ctypes.c_uint))[0] = 1024 ctypes.cast(height_return, ctypes.POINTER(ctypes.c_uint))[0] = 768 return 1 def XResizeWindow( self, _display: object, window: int, width: int, height: int, ) -> int: self.events.append(("resize", window, width, height)) return 0 def XSync(self, _display: object, discard: int) -> int: self.events.append(("sync", discard)) return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with ( mock.patch.object( mapper, "_wait_for_window_size", return_value=True, ), mock.patch( "ptlinux.gamebox_runtime.time.sleep", side_effect=RuntimeError("interrupted"), ), self.assertRaisesRegex(RuntimeError, "interrupted"), ): mapper.refresh_render_surface(12) self.assertEqual( library.events, [ ("resize", 12, 1023, 768), ("sync", 0), ("resize", 12, 1024, 768), ("sync", 0), ], ) def test_render_surface_refresh_declines_when_geometry_is_unavailable(self) -> None: library = mock.Mock() library.XGetGeometry.return_value = 0 mapper = object.__new__(_X11) mapper.library = library mapper.display = ctypes.c_void_p(1) self.assertFalse(mapper.refresh_render_surface(12)) library.XResizeWindow.assert_not_called() library.XSync.assert_not_called() def test_render_surface_refresh_reports_a_declined_nudge(self) -> None: class FakeX11Library: def __init__(self) -> None: self.sizes: list[tuple[int, int]] = [] def XResizeWindow( self, _display: object, _window: int, width: int, height: int, ) -> int: self.sizes.append((width, height)) return 0 def XSync(self, _display: object, _discard: int) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with mock.patch.object( mapper, "_wait_for_window_size", side_effect=[False, True], ): self.assertFalse( mapper.refresh_render_surface(12, target_size=(1280, 960)) ) self.assertEqual(library.sizes, [(1279, 960), (1280, 960)]) def test_render_surface_refresh_nudges_height_for_a_one_pixel_width(self) -> None: class FakeX11Library: def __init__(self) -> None: self.sizes: list[tuple[int, int]] = [] def XGetGeometry( self, _display: object, _window: int, _root_return: Any, _x_return: Any, _y_return: Any, width_return: Any, height_return: Any, _border_width_return: Any, _depth_return: Any, ) -> int: ctypes.cast(width_return, ctypes.POINTER(ctypes.c_uint))[0] = 1 ctypes.cast(height_return, ctypes.POINTER(ctypes.c_uint))[0] = 2 return 1 def XResizeWindow( self, _display: object, _window: int, width: int, height: int, ) -> int: self.sizes.append((width, height)) return 0 def XSync(self, _display: object, _discard: int) -> int: return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) with ( mock.patch.object( mapper, "_wait_for_window_size", return_value=True, ), mock.patch("ptlinux.gamebox_runtime.time.sleep"), ): self.assertTrue(mapper.refresh_render_surface(12)) self.assertEqual(library.sizes, [(1, 1), (1, 2)]) def test_render_surface_refresh_declines_an_unnudgeable_window(self) -> None: def get_one_by_one_geometry( _display: object, _window: int, _root_return: Any, _x_return: Any, _y_return: Any, width_return: Any, height_return: Any, _border_width_return: Any, _depth_return: Any, ) -> int: ctypes.cast(width_return, ctypes.POINTER(ctypes.c_uint))[0] = 1 ctypes.cast(height_return, ctypes.POINTER(ctypes.c_uint))[0] = 1 return 1 library = mock.Mock() library.XGetGeometry.side_effect = get_one_by_one_geometry mapper = object.__new__(_X11) mapper.library = library mapper.display = ctypes.c_void_p(1) self.assertFalse(mapper.refresh_render_surface(12)) library.XResizeWindow.assert_not_called() library.XSync.assert_not_called() def test_focus_window_raises_then_focuses_the_exact_client(self) -> None: class FakeX11Library: def __init__(self) -> None: self.events: list[object] = [] def XMapRaised(self, _display: object, window: int) -> int: self.events.append(("raise", window)) return 0 def XSetInputFocus( self, _display: object, window: int, revert_to: int, timestamp: int, ) -> int: self.events.append(("focus", window, revert_to, timestamp)) return 0 def XSync(self, _display: object, discard: int) -> int: self.events.append(("sync", discard)) return 0 library = FakeX11Library() mapper = object.__new__(_X11) mapper.library = library # type: ignore[assignment] mapper.display = ctypes.c_void_p(1) mapper.focus_window(12) self.assertEqual( library.events, [("raise", 12), ("focus", 12, 2, 0), ("sync", 0)], ) def test_restore_input_focus_does_not_raise_the_client(self) -> None: library = mock.Mock() mapper = object.__new__(_X11) mapper.library = library mapper.display = ctypes.c_void_p(1) mapper.restore_input_focus(12) library.XMapRaised.assert_not_called() library.XSetInputFocus.assert_called_once_with( mapper.display, 12, 2, 0, ) library.XSync.assert_called_once_with(mapper.display, 0) def test_game_focus_waits_for_the_dashboard_handoff(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 mapper.window_is_active.return_value = True mapper.window_is_focused.side_effect = [False, True] with mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep: _focus_game_window(mapper, 12) sleep.assert_called_once_with(WINDOW_FOCUS_DELAY_SECONDS) mapper.activate_window.assert_not_called() mapper.restore_input_focus.assert_called_once_with(12) def test_host_minimize_request_never_raises_the_window(self) -> None: mapper = object.__new__(_X11) mapper.library = mock.Mock() mapper.library.XDefaultScreen.return_value = 2 mapper.library.XIconifyWindow.return_value = 1 mapper.display = ctypes.c_void_p(1) self.assertTrue(mapper.minimize_window(12)) mapper.library.XIconifyWindow.assert_called_once_with( mapper.display, 12, 2, ) mapper.library.XMapRaised.assert_not_called() mapper.library.XSync.assert_called_once_with(mapper.display, 0) def test_game_focus_failure_is_nonfatal(self) -> None: mapper = mock.Mock() mapper.window_state.side_effect = RuntimeError("test failure") output = io.StringIO() with ( mock.patch("ptlinux.gamebox_runtime.time.sleep"), contextlib.redirect_stderr(output), ): _focus_game_window(mapper, 12) self.assertIn("continuing normally", output.getvalue()) def test_focus_repair_does_not_reactivate_a_background_window(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 mapper.window_is_active.return_value = False self.assertFalse(_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_focus_repair_restores_input_without_raising_the_active_window(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 mapper.window_is_active.return_value = True mapper.window_is_focused.side_effect = [False, True] self.assertTrue(_repair_game_window_focus(mapper, 12)) mapper.restore_input_focus.assert_called_once_with(12) mapper.activate_window.assert_not_called() mapper.focus_window.assert_not_called() def test_window_scaling_failure_is_nonfatal(self) -> None: mapper = mock.Mock() mapper.enable_window_scaling.side_effect = RuntimeError("test failure") output = io.StringIO() with contextlib.redirect_stderr(output): self.assertFalse(_enable_game_window_scaling(mapper, 12)) self.assertIn("Warning", output.getvalue()) self.assertIn("continue normally", output.getvalue()) def test_scaling_monitor_failure_leaves_a_consistent_fixed_window(self) -> None: mapper = mock.Mock() mapper.select_scaling_hint_events.side_effect = RuntimeError("test failure") process = mock.Mock() process.wait.return_value = 0 output = io.StringIO() with contextlib.redirect_stderr(output): self.assertEqual( _maintain_game_window_scaling(mapper, process, 12), 0, ) mapper.enable_window_scaling.assert_not_called() self.assertIn("fixed-size for consistency", output.getvalue()) process.wait.assert_called_once_with() def test_scaling_maintenance_reapplies_hints_after_wine_replaces_them( self, ) -> None: mapper = mock.Mock() mapper.enable_window_scaling.return_value = True mapper.wait_for_scaling_hint_change.side_effect = [True, False] process = mock.Mock() process.poll.side_effect = [None, None, 0] process.wait.return_value = 0 with contextlib.redirect_stderr(io.StringIO()): self.assertEqual( _maintain_game_window_scaling(mapper, process, 12), 0, ) mapper.select_scaling_hint_events.assert_called_once_with(12) self.assertEqual( mapper.enable_window_scaling.call_args_list, [ mock.call(12), mock.call(12, confirm=False), ], ) process.wait.assert_called_once_with() def test_scaling_maintenance_stops_after_repeated_repair_failures(self) -> None: mapper = mock.Mock() mapper.enable_window_scaling.side_effect = [ True, *[RuntimeError("test failure")] * WINDOW_SCALING_REPAIR_LIMIT, ] mapper.wait_for_scaling_hint_change.return_value = True process = mock.Mock() process.poll.return_value = None process.wait.return_value = 0 output = io.StringIO() with ( contextlib.redirect_stderr(output), mock.patch("ptlinux.gamebox_runtime._maintain_game_window_focus", side_effect=lambda _mapper, child, _window, **_kwargs: child.wait()), ): self.assertEqual( _maintain_game_window_scaling(mapper, process, 12), 0, ) self.assertEqual( mapper.enable_window_scaling.call_count, 1 + WINDOW_SCALING_REPAIR_LIMIT, ) self.assertIn("disabled after repeated failures", output.getvalue()) process.wait.assert_called_once_with() def test_fixed_window_maintenance_preserves_the_focus_repair(self) -> None: mapper = mock.Mock() process = mock.Mock() process.poll.side_effect = [None, None, 0] process.wait.return_value = 0 output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.5, 1.0, 1.0], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, mock.patch( "ptlinux.gamebox_runtime._repair_game_window_focus" ) as repair_focus, contextlib.redirect_stderr(output), ): self.assertEqual( _maintain_game_window_focus(mapper, process, 12), 0, ) sleep.assert_has_calls([mock.call(0.5), mock.call(0.5)]) repair_focus.assert_called_once_with(mapper, 12) mapper.enable_window_scaling.assert_not_called() mapper.select_scaling_hint_events.assert_not_called() process.wait.assert_called_once_with() self.assertIn("focus monitoring active", output.getvalue()) def test_gamescope_maintenance_repairs_focus_without_changing_geometry( self, ) -> None: mapper = mock.Mock() process = mock.Mock() process.poll.side_effect = [None, None, 0] process.wait.return_value = 0 with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.1, 0.25, 0.25], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, mock.patch( "ptlinux.gamebox_runtime._repair_gamescope_game_window_focus" ) as repair_focus, ): self.assertEqual( _maintain_gamescope_game_window_focus(mapper, process, 12), 0, ) sleep.assert_has_calls( [ mock.call(GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS), mock.call(GAMESCOPE_FOCUS_REPAIR_INTERVAL_SECONDS), ] ) repair_focus.assert_called_once_with(mapper, 12) mapper.enable_window_scaling.assert_not_called() mapper.select_scaling_hint_events.assert_not_called() process.wait.assert_called_once_with() def test_gamescope_focus_repair_never_activates_or_raises_the_host(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 mapper.window_is_focused.side_effect = [False, True] self.assertTrue(_repair_gamescope_game_window_focus(mapper, 12)) mapper.restore_input_focus.assert_called_once_with(12) mapper.activate_window.assert_not_called() mapper.focus_window.assert_not_called() def test_gamescope_focus_repair_stops_while_the_host_is_inactive(self) -> None: mapper = mock.Mock() with tempfile.TemporaryDirectory() as temporary: state_file = Path(temporary) / "focus" state_file.write_text("0 100.0\n", encoding="ascii") with mock.patch.dict( os.environ, {GAMESCOPE_HOST_FOCUS_STATE_ENV: str(state_file)}, ): self.assertFalse(_repair_gamescope_game_window_focus(mapper, 12)) mapper.window_state.assert_not_called() mapper.restore_input_focus.assert_not_called() def test_gamescope_host_focus_state_must_be_active_and_fresh(self) -> None: with tempfile.TemporaryDirectory() as temporary: state_file = Path(temporary) / "focus" environment = {GAMESCOPE_HOST_FOCUS_STATE_ENV: str(state_file)} state_file.write_text("1 100.0\n", encoding="ascii") self.assertTrue( _gamescope_host_focus_is_allowed( environment=environment, now=100.1, ) ) self.assertFalse( _gamescope_host_focus_is_allowed( environment=environment, now=100.0 + GAMESCOPE_HOST_FOCUS_STATE_MAX_AGE_SECONDS + 0.01, ) ) state_file.write_text("0 101.0\n", encoding="ascii") self.assertFalse( _gamescope_host_focus_is_allowed( environment=environment, now=101.0, ) ) def test_gamescope_host_focus_defaults_to_legacy_behavior_without_bridge( self, ) -> None: self.assertTrue(_gamescope_host_focus_is_allowed(environment={}, now=10.0)) def test_gamescope_resolver_keeps_a_normal_window_on_the_fast_path(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 self.assertEqual( _resolve_gamescope_game_window( mapper, 12, window_title="PristonTale EU", excluded_window_ids={4}, ), 12, ) mapper.find_first_window.assert_not_called() def test_gamescope_resolver_recovers_a_nested_login_iconify(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 3 mapper.restore_normal_window_state.return_value = True output = io.StringIO() with contextlib.redirect_stderr(output): self.assertEqual( _resolve_gamescope_game_window( mapper, 12, window_title="PristonTale EU", excluded_window_ids={4}, ), 12, ) mapper.restore_normal_window_state.assert_called_once_with(12) mapper.find_first_window.assert_not_called() mapper.activate_window.assert_not_called() mapper.focus_window.assert_not_called() self.assertIn("surface visible inside Gamescope", output.getvalue()) def test_gamescope_resolver_follows_replacement_after_iconify_recovery_fails( self, ) -> None: mapper = mock.Mock() mapper.window_state.return_value = 3 mapper.restore_normal_window_state.return_value = False mapper.find_first_window.return_value = (21, "PristonTale EU V21.77.86") with mock.patch( "ptlinux.gamebox_runtime._apply_game_window_identity" ) as apply_identity, contextlib.redirect_stderr(io.StringIO()): self.assertEqual( _resolve_gamescope_game_window( mapper, 12, window_title="PristonTale EU", excluded_window_ids={4}, ), 21, ) mapper.find_first_window.assert_called_once_with( "PristonTale EU", excluded_window_ids={4, 12}, ) apply_identity.assert_called_once_with(mapper, 21) def test_gamescope_resolver_follows_a_replacement_login_window(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = None mapper.find_first_window.return_value = ( 21, "PristonTale EU V21.77.86", ) output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime._apply_game_window_identity" ) as apply_identity, contextlib.redirect_stderr(output), ): self.assertEqual( _resolve_gamescope_game_window( mapper, 12, window_title="PristonTale EU", excluded_window_ids={4}, ), 21, ) mapper.find_first_window.assert_called_once_with( "PristonTale EU", excluded_window_ids={4, 12}, ) apply_identity.assert_called_once_with(mapper, 21) self.assertIn("following it", output.getvalue()) def test_gamescope_resolver_never_focuses_a_stale_window(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = None mapper.window_size.return_value = None mapper.find_first_window.return_value = None self.assertIsNone( _resolve_gamescope_game_window( mapper, 12, window_title="PristonTale EU", excluded_window_ids=set(), ) ) mapper.restore_input_focus.assert_not_called() mapper.focus_window.assert_not_called() def test_startup_guard_confirms_stability_only_after_its_deadline(self) -> None: mapper = mock.Mock() process = mock.Mock() process.poll.return_value = None with mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 12.0], ): self.assertTrue( _guard_game_window_startup( mapper, process, 12, borderless=False, ) ) mapper.window_state.assert_not_called() def test_startup_guard_respects_a_minimized_window(self) -> None: mapper = mock.Mock() mapper.window_is_borderless.return_value = True mapper.window_state.return_value = 3 mapper.window_is_fullscreen_or_maximized.return_value = False process = mock.Mock() process.poll.return_value = None output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.1], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, mock.patch("ptlinux.gamebox_runtime._prepare_game_window") as prepare, contextlib.redirect_stderr(output), ): self.assertFalse( _guard_game_window_startup( mapper, process, 12, borderless=True, ) ) mapper.activate_window.assert_not_called() prepare.assert_not_called() mapper.focus_window.assert_not_called() sleep.assert_not_called() self.assertIn("leaving it minimized", output.getvalue()) def test_startup_guard_recovers_an_early_login_style_reset_while_normal(self) -> None: mapper = mock.Mock() mapper.window_is_borderless.side_effect = [False, False, True] mapper.window_state.return_value = 1 mapper.window_is_fullscreen_or_maximized.return_value = False process = mock.Mock() process.poll.side_effect = [None, 0] output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.1], ), mock.patch("ptlinux.gamebox_runtime.time.sleep"), mock.patch("ptlinux.gamebox_runtime._prepare_game_window") as prepare, contextlib.redirect_stderr(output), ): _guard_game_window_startup( mapper, process, 12, borderless=True, ) prepare.assert_called_once() self.assertIn("changed its window layout", output.getvalue()) def test_startup_guard_leaves_a_stable_window_untouched(self) -> None: mapper = mock.Mock() mapper.window_is_borderless.return_value = True mapper.window_state.return_value = 1 mapper.window_is_fullscreen_or_maximized.return_value = False process = mock.Mock() process.poll.side_effect = [None, 0] with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.1], ), mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep, ): _guard_game_window_startup( mapper, process, 12, borderless=True, ) mapper.activate_window.assert_not_called() mapper.focus_window.assert_not_called() sleep.assert_called_once_with(WINDOW_STARTUP_POLL_SECONDS) def test_startup_guard_recovers_reapplied_fullscreen_in_decorated_mode(self) -> None: mapper = mock.Mock() mapper.window_state.return_value = 1 mapper.window_is_fullscreen_or_maximized.return_value = True process = mock.Mock() process.poll.side_effect = [None, 0] output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime.time.monotonic", side_effect=[0.0, 0.1], ), mock.patch("ptlinux.gamebox_runtime.time.sleep"), mock.patch("ptlinux.gamebox_runtime._prepare_game_window") as prepare, contextlib.redirect_stderr(output), ): _guard_game_window_startup( mapper, process, 12, borderless=False, ) prepare.assert_called_once_with( mapper, 12, borderless=False, suppress_wm_ping=False, raise_window=False, ) self.assertIn("entered fullscreen/maximized mode", output.getvalue()) def test_wm_ping_failure_does_not_skip_normal_window_mapping(self) -> None: events: list[object] = [] output = io.StringIO() class FakeMapper: def suppress_wm_ping(self, window: int) -> bool: events.append(("suppress", window)) raise RuntimeError("test failure") def map_window(self, window: int, *, borderless: bool) -> None: events.append(("map", window, borderless)) def force_windowed_state(self, window: int) -> bool: events.append(("windowed", window)) return False def refresh_render_surface(self, window: int) -> bool: events.append(("refresh", window)) return True with contextlib.redirect_stderr(output): _prepare_game_window( FakeMapper(), # type: ignore[arg-type] 12, borderless=True, suppress_wm_ping=True, ) self.assertEqual( events, [ ("suppress", 12), ("map", 12, True), ("windowed", 12), ("refresh", 12), ], ) self.assertIn("continuing normally", output.getvalue()) def test_borderless_preparation_restores_the_pre_reframe_size(self) -> None: events: list[object] = [] class FakeMapper: def map_window( self, window: int, *, borderless: bool, ) -> tuple[int, int]: events.append(("map", window, borderless)) return 1024, 768 def force_windowed_state(self, window: int) -> bool: events.append(("windowed", window)) return False def refresh_render_surface( self, window: int, *, target_size: tuple[int, int] | None = None, ) -> bool: events.append(("refresh", window, target_size)) return True with mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep: _prepare_game_window( FakeMapper(), # type: ignore[arg-type] 12, borderless=True, suppress_wm_ping=False, ) sleep.assert_called_once_with(WINDOW_SURFACE_SETTLE_SECONDS) self.assertEqual( events, [ ("map", 12, True), ("windowed", 12), ("refresh", 12, (1024, 768)), ], ) def test_decorated_window_resynchronizes_its_render_surface(self) -> None: events: list[object] = [] class FakeMapper: def map_window( self, window: int, *, borderless: bool, ) -> tuple[int, int]: events.append(("map", window, borderless)) return 1280, 960 def force_windowed_state(self, window: int) -> bool: events.append(("windowed", window)) return False def refresh_render_surface( self, window: int, *, target_size: tuple[int, int] | None = None, ) -> bool: events.append(("refresh", window, target_size)) return True with mock.patch("ptlinux.gamebox_runtime.time.sleep") as sleep: _prepare_game_window( FakeMapper(), # type: ignore[arg-type] 12, borderless=False, suppress_wm_ping=False, ) sleep.assert_called_once_with(WINDOW_SURFACE_SETTLE_SECONDS) self.assertEqual( events, [ ("map", 12, False), ("windowed", 12), ("refresh", 12, (1280, 960)), ], ) def test_windowed_preparation_uses_the_geometry_restored_by_the_wm(self) -> None: events: list[object] = [] class FakeMapper: def map_window( self, window: int, *, borderless: bool, ) -> tuple[int, int]: events.append(("map", window, borderless)) return 1920, 1080 def force_windowed_state(self, window: int) -> bool: events.append(("windowed", window)) return True def window_size(self, window: int) -> tuple[int, int]: events.append(("size", window)) return 1280, 960 def refresh_render_surface( self, window: int, *, target_size: tuple[int, int] | None = None, ) -> bool: events.append(("refresh", window, target_size)) return True with mock.patch("ptlinux.gamebox_runtime.time.sleep"): _prepare_game_window( FakeMapper(), # type: ignore[arg-type] 12, borderless=False, suppress_wm_ping=False, ) self.assertEqual( events, [ ("map", 12, False), ("windowed", 12), ("size", 12), ("refresh", 12, (1280, 960)), ], ) def test_surface_refresh_failure_does_not_abort_window_mapping(self) -> None: events: list[object] = [] output = io.StringIO() class FakeMapper: def map_window(self, window: int, *, borderless: bool) -> None: events.append(("map", window, borderless)) def force_windowed_state(self, window: int) -> bool: events.append(("windowed", window)) return False def refresh_render_surface(self, window: int) -> bool: events.append(("refresh", window)) raise RuntimeError("test failure") with contextlib.redirect_stderr(output): _prepare_game_window( FakeMapper(), # type: ignore[arg-type] 12, borderless=True, suppress_wm_ping=False, ) self.assertEqual( events, [("map", 12, True), ("windowed", 12), ("refresh", 12)], ) self.assertIn("continuing normally", output.getvalue()) def test_snapshots_existing_windows_before_starting_game(self) -> None: events: list[object] = [] class FakeMapper: def __init__(self) -> None: self.events = events self.events.append("mapper") self.active = False def root_window_ids(self) -> set[int]: self.events.append("snapshot") return {10} def find_first_window( self, _title: str, *, excluded_window_ids: Collection[int], ) -> tuple[int, str]: self.events.append(("find", set(excluded_window_ids))) return 12, "PristonTale EU V21.75.83" def set_window_identity(self, window: int) -> None: self.events.append(("identity", window)) def map_window(self, window: int, *, borderless: bool, raise_window: bool = True) -> None: assert not raise_window self.events.append(("map", window, borderless)) def force_windowed_state(self, window: int) -> bool: self.events.append(("windowed", window)) return False def refresh_render_surface(self, window: int) -> bool: self.events.append(("refresh", window)) return True def focus_window(self, window: int) -> None: self.events.append(("focus", window)) def window_state(self, _window: int) -> int: return 1 def activate_window(self, window: int) -> bool: self.events.append(("activate", window)) self.active = True return True def window_is_active(self, _window: int) -> bool: return self.active def window_is_focused(self, _window: int) -> bool: return False def restore_input_focus(self, window: int) -> None: self.events.append(("focus", window)) def suppress_wm_ping(self, window: int) -> bool: self.events.append(("suppress", window)) return True def close(self) -> None: self.events.append("close") class FakeProcess: def __init__(self) -> None: self.events = events self.finished = False def poll(self) -> int | None: self.events.append("poll") return 0 if self.finished else None def wait(self) -> int: self.events.append("wait") return 0 def start_process(_command: list[str]) -> FakeProcess: events.append("process") return FakeProcess() def guard_startup( _mapper: object, _process: FakeProcess, window: int, *, borderless: bool, ) -> None: events.append(("guard", window, borderless)) _process.finished = True with ( mock.patch("ptlinux.gamebox_runtime._X11", FakeMapper), mock.patch("ptlinux.gamebox_runtime.subprocess.Popen", start_process), mock.patch( "ptlinux.gamebox_runtime._guard_game_window_startup", guard_startup, ), mock.patch("ptlinux.gamebox_runtime.WINDOW_READY_DELAY_SECONDS", 0.0), mock.patch("ptlinux.gamebox_runtime.WINDOW_FOCUS_DELAY_SECONDS", 0.0), ): self.assertEqual( run_game_window( ["game"], borderless=True, suppress_wm_ping=True, ), 0, ) self.assertLess(events.index("snapshot"), events.index("process")) self.assertIn(("find", {10}), events) self.assertLess(events.index(("identity", 12)), events.index(("map", 12, True))) self.assertLess(events.index(("suppress", 12)), events.index(("map", 12, True))) self.assertIn(("map", 12, True), events) self.assertLess( events.index(("map", 12, True)), events.index(("windowed", 12)), ) self.assertLess(events.index(("windowed", 12)), events.index(("refresh", 12))) self.assertLess(events.index(("refresh", 12)), events.index(("focus", 12))) self.assertLess(events.index(("activate", 12)), events.index(("focus", 12))) self.assertLess( events.index(("focus", 12)), events.index(("guard", 12, True)), ) self.assertLess(events.index(("guard", 12, True)), events.index("wait")) self.assertEqual(events[-1], "close") def test_keeps_a_decorated_window_fixed_by_default(self) -> None: mapper = mock.Mock() mapper.root_window_ids.return_value = set() mapper.find_first_window.return_value = ( 12, "PristonTale EU V21.75.83", ) process = mock.Mock() process.poll.return_value = None process.wait.return_value = 0 with ( mock.patch("ptlinux.gamebox_runtime._X11", return_value=mapper), mock.patch( "ptlinux.gamebox_runtime.subprocess.Popen", return_value=process, ), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", return_value=0.0, ), mock.patch("ptlinux.gamebox_runtime.WINDOW_READY_DELAY_SECONDS", 0.0), mock.patch("ptlinux.gamebox_runtime._prepare_game_window"), mock.patch("ptlinux.gamebox_runtime._focus_game_window"), mock.patch( "ptlinux.gamebox_runtime._guard_game_window_startup", return_value=True, ) as guard, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_scaling", return_value=0, ) as maintain_scaling, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_focus", return_value=0, ) as maintain_focus, ): self.assertEqual( run_game_window(["game"]), 0, ) guard.assert_called_once_with(mapper, process, 12, borderless=False) maintain_scaling.assert_not_called() maintain_focus.assert_called_once_with( mapper, process, 12, window_title=WINDOW_TITLE, existing_window_ids=set(), expected_pid=mapper.window_pid.return_value, ) mapper.close.assert_called_once_with() def test_enables_scaling_only_for_an_explicit_opt_in_after_stable_startup( self, ) -> None: mapper = mock.Mock() mapper.root_window_ids.return_value = set() mapper.find_first_window.return_value = ( 12, "PristonTale EU V21.75.83", ) process = mock.Mock() process.poll.return_value = None process.wait.return_value = 0 with ( mock.patch("ptlinux.gamebox_runtime._X11", return_value=mapper), mock.patch( "ptlinux.gamebox_runtime.subprocess.Popen", return_value=process, ), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", return_value=0.0, ), mock.patch("ptlinux.gamebox_runtime.WINDOW_READY_DELAY_SECONDS", 0.0), mock.patch("ptlinux.gamebox_runtime._prepare_game_window"), mock.patch("ptlinux.gamebox_runtime._focus_game_window"), mock.patch( "ptlinux.gamebox_runtime._guard_game_window_startup", return_value=True, ), mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_scaling", return_value=0, ) as maintain_scaling, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_focus", return_value=0, ) as maintain_focus, ): self.assertEqual( run_game_window(["game"], allow_window_resizing=True), 0, ) maintain_scaling.assert_called_once_with( mapper, process, 12, window_title=WINDOW_TITLE, existing_window_ids=set(), expected_pid=mapper.window_pid.return_value, ) maintain_focus.assert_not_called() mapper.close.assert_called_once_with() def test_gamescope_scaling_leaves_geometry_to_gamescope_and_repairs_focus( self, ) -> None: mapper = mock.Mock() mapper.root_window_ids.return_value = set() mapper.find_first_window.return_value = ( 12, "PristonTale EU V21.75.83", ) process = mock.Mock() process.poll.return_value = None process.wait.return_value = 0 with ( mock.patch("ptlinux.gamebox_runtime._X11", return_value=mapper), mock.patch( "ptlinux.gamebox_runtime.subprocess.Popen", return_value=process, ), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", return_value=0.0, ), mock.patch("ptlinux.gamebox_runtime.WINDOW_READY_DELAY_SECONDS", 0.0), mock.patch( "ptlinux.gamebox_runtime._prepare_game_window" ) as prepare, mock.patch( "ptlinux.gamebox_runtime._focus_game_window" ) as focus, mock.patch( "ptlinux.gamebox_runtime._guard_game_window_startup", return_value=True, ) as guard, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_scaling", return_value=0, ) as maintain_scaling, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_focus", return_value=0, ) as maintain_focus, mock.patch( "ptlinux.gamebox_runtime._maintain_gamescope_game_window_focus", return_value=0, ) as maintain_gamescope_focus, ): self.assertEqual( run_game_window( ["game"], allow_window_resizing=True, gamescope_scaling=True, ), 0, ) maintain_scaling.assert_not_called() maintain_focus.assert_not_called() maintain_gamescope_focus.assert_called_once_with( mapper, process, 12, window_title="PristonTale EU", existing_window_ids=set(), ) prepare.assert_not_called() guard.assert_not_called() focus.assert_called_once_with(mapper, 12, activate=False) process.wait.assert_not_called() mapper.close.assert_called_once_with() def test_does_not_enable_host_scaling_for_a_borderless_window(self) -> None: mapper = mock.Mock() mapper.root_window_ids.return_value = set() mapper.find_first_window.return_value = ( 12, "PristonTale EU V21.75.83", ) process = mock.Mock() process.poll.return_value = None process.wait.return_value = 0 with ( mock.patch("ptlinux.gamebox_runtime._X11", return_value=mapper), mock.patch( "ptlinux.gamebox_runtime.subprocess.Popen", return_value=process, ), mock.patch( "ptlinux.gamebox_runtime.time.monotonic", return_value=0.0, ), mock.patch("ptlinux.gamebox_runtime.WINDOW_READY_DELAY_SECONDS", 0.0), mock.patch("ptlinux.gamebox_runtime._prepare_game_window"), mock.patch("ptlinux.gamebox_runtime._focus_game_window"), mock.patch( "ptlinux.gamebox_runtime._guard_game_window_startup", return_value=True, ) as guard, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_scaling" ) as maintain_scaling, mock.patch( "ptlinux.gamebox_runtime._maintain_game_window_focus", return_value=0, ) as maintain_focus, ): self.assertEqual( run_game_window(["game"], borderless=True), 0, ) guard.assert_called_once_with(mapper, process, 12, borderless=True) maintain_scaling.assert_not_called() maintain_focus.assert_called_once_with( mapper, process, 12, window_title=WINDOW_TITLE, existing_window_ids=set(), expected_pid=mapper.window_pid.return_value, ) mapper.close.assert_called_once_with() def test_unavailable_x11_does_not_delay_or_abort_the_game(self) -> None: process = mock.Mock() process.wait.return_value = 0 output = io.StringIO() with ( mock.patch( "ptlinux.gamebox_runtime._X11", side_effect=GameboxRuntimeError("could not open the host X11 display"), ), mock.patch( "ptlinux.gamebox_runtime.subprocess.Popen", return_value=process, ) as start_process, contextlib.redirect_stderr(output), ): self.assertEqual(run_game_window(["game"]), 0) start_process.assert_called_once_with(["game"]) process.wait.assert_called_once_with() self.assertIn("continuing without X11 window styling", output.getvalue()) self.assertIn("Game window monitoring unavailable; process started:", output.getvalue()) self.assertNotIn("Game window ready:", output.getvalue()) if __name__ == "__main__": unittest.main()