2555 lines
68 KiB
Python
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()
|