Files
pristontale-eu-linux/tests/test_ptlinux_gamebox_runtime.py

2555 lines
68 KiB
Python

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()