Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
830
tests/test_ptlinux_gamescope_runtime.py
Normal file
830
tests/test_ptlinux_gamescope_runtime.py
Normal file
@@ -0,0 +1,830 @@
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
from ptlinux.gamescope_runtime import (
|
||||
_HostFocusPublisher,
|
||||
_bundled_gamescope_is_usable,
|
||||
_is_cinnamon_session,
|
||||
_is_kde_session,
|
||||
_process_exit_status,
|
||||
_read_gamescope_version,
|
||||
_select_host_backend,
|
||||
_select_wayland_gamescope,
|
||||
_supervised_command,
|
||||
_tune_process_window,
|
||||
_wait_for_optimized_gamescope,
|
||||
main,
|
||||
resize_increments,
|
||||
run_gamescope,
|
||||
)
|
||||
from ptlinux.gamebox_runtime import GAMESCOPE_HOST_FOCUS_STATE_ENV
|
||||
from ptlinux.window_diagnostics import (
|
||||
WINDOW_DIAGNOSTICS_ENV,
|
||||
WINDOW_DIAGNOSTICS_SESSION_ENV,
|
||||
WindowDiagnosticRecorder,
|
||||
parse_window_diagnostic,
|
||||
)
|
||||
|
||||
|
||||
class GamescopeRuntimeTests(unittest.TestCase):
|
||||
def test_uses_five_percent_resize_steps_with_sensible_bounds(self) -> None:
|
||||
self.assertEqual(resize_increments(1280, 960), (64, 48))
|
||||
self.assertEqual(resize_increments(100, 100), (8, 8))
|
||||
self.assertEqual(resize_increments(7680, 4320), (96, 96))
|
||||
|
||||
def test_rejects_invalid_canvas_dimensions(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "positive"):
|
||||
resize_increments(0, 960)
|
||||
|
||||
def test_inserts_selected_backend_before_existing_gamescope_options(self) -> None:
|
||||
self.assertEqual(
|
||||
_supervised_command(
|
||||
["/usr/bin/gamescope", "-w", "1280", "--", "game"],
|
||||
Path("/tmp/status"),
|
||||
"wayland",
|
||||
),
|
||||
[
|
||||
"/usr/bin/gamescope",
|
||||
"--backend",
|
||||
"wayland",
|
||||
"-w",
|
||||
"1280",
|
||||
"--",
|
||||
mock.ANY,
|
||||
"-P",
|
||||
"-m",
|
||||
"ptlinux.gamescope_child_runtime",
|
||||
"--status-file",
|
||||
"/tmp/status",
|
||||
"--",
|
||||
"game",
|
||||
],
|
||||
)
|
||||
|
||||
def test_can_force_composition_for_an_unsafe_wayland_build(self) -> None:
|
||||
command = _supervised_command(
|
||||
["gamescope", "--", "game"],
|
||||
Path("/tmp/status"),
|
||||
"wayland",
|
||||
force_composition=True,
|
||||
)
|
||||
|
||||
self.assertEqual(command[:4], [
|
||||
"gamescope",
|
||||
"--backend",
|
||||
"wayland",
|
||||
"--force-composition",
|
||||
])
|
||||
|
||||
def test_reads_the_gamescope_release_tuple(self) -> None:
|
||||
completed = mock.Mock(
|
||||
returncode=0,
|
||||
stdout="[gamescope] gamescope version 3.16.20+ds-1 (gcc 15.2.0)\n"
|
||||
)
|
||||
with mock.patch(
|
||||
"ptlinux.gamescope_runtime.subprocess.run",
|
||||
return_value=completed,
|
||||
) as run:
|
||||
self.assertEqual(
|
||||
_read_gamescope_version("/usr/games/gamescope"),
|
||||
(3, 16, 20),
|
||||
)
|
||||
|
||||
run.assert_called_once_with(
|
||||
["/usr/games/gamescope", "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=1.0,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_selects_upstream_or_launcher_local_wayland_backing_fix(self) -> None:
|
||||
with mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_gamescope_version",
|
||||
side_effect=[(3, 16, 28), (3, 16, 20)],
|
||||
), mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_is_usable",
|
||||
return_value=True,
|
||||
), mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_path",
|
||||
return_value=Path("/opt/ptlinux/gamescope"),
|
||||
):
|
||||
self.assertEqual(
|
||||
_select_wayland_gamescope("/usr/games/gamescope"),
|
||||
("/usr/games/gamescope", False, False),
|
||||
)
|
||||
self.assertEqual(
|
||||
_select_wayland_gamescope("/usr/games/gamescope"),
|
||||
("/opt/ptlinux/gamescope", False, True),
|
||||
)
|
||||
|
||||
def test_unsafe_wayland_build_uses_composed_fallback(self) -> None:
|
||||
with (
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_gamescope_version",
|
||||
return_value=(3, 16, 20),
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_is_usable",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
self.assertEqual(
|
||||
_select_wayland_gamescope("gamescope"),
|
||||
("gamescope", True, False),
|
||||
)
|
||||
|
||||
def test_kubuntu_keeps_every_native_wayland_build_fully_composed(self) -> None:
|
||||
environment = {
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"XDG_SESSION_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasmawayland",
|
||||
}
|
||||
with (
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_gamescope_version",
|
||||
return_value=(3, 16, 28),
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_is_usable",
|
||||
return_value=True,
|
||||
) as bundled_gamescope,
|
||||
):
|
||||
self.assertEqual(
|
||||
_select_wayland_gamescope(
|
||||
"/usr/games/gamescope",
|
||||
environment=environment,
|
||||
),
|
||||
("/usr/games/gamescope", True, False),
|
||||
)
|
||||
|
||||
self.assertTrue(_is_kde_session(environment))
|
||||
bundled_gamescope.assert_not_called()
|
||||
|
||||
def test_recognizes_cinnamon_session_names(self) -> None:
|
||||
self.assertTrue(
|
||||
_is_cinnamon_session(
|
||||
{
|
||||
"XDG_CURRENT_DESKTOP": "X-Cinnamon:GNOME",
|
||||
"DESKTOP_SESSION": "cinnamon2d",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertFalse(_is_cinnamon_session({"XDG_CURRENT_DESKTOP": "GNOME"}))
|
||||
|
||||
def test_launcher_local_gamescope_matches_its_pinned_digest(self) -> None:
|
||||
with mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_host_supported",
|
||||
return_value=True,
|
||||
):
|
||||
self.assertTrue(_bundled_gamescope_is_usable())
|
||||
|
||||
def test_repairs_a_non_executable_verified_launcher_local_gamescope(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
executable = Path(temporary) / "gamescope"
|
||||
payload = b"verified launcher-local gamescope"
|
||||
executable.write_bytes(payload)
|
||||
executable.chmod(0o644)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_host_supported",
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_path",
|
||||
return_value=executable,
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.BUNDLED_GAMESCOPE_SHA256",
|
||||
hashlib.sha256(payload).hexdigest(),
|
||||
),
|
||||
):
|
||||
self.assertTrue(_bundled_gamescope_is_usable())
|
||||
|
||||
self.assertEqual(executable.stat().st_mode & 0o777, 0o755)
|
||||
|
||||
def test_does_not_make_an_unverified_launcher_local_gamescope_executable(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
executable = Path(temporary) / "gamescope"
|
||||
executable.write_bytes(b"unexpected contents")
|
||||
executable.chmod(0o644)
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_host_supported",
|
||||
return_value=True,
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._bundled_gamescope_path",
|
||||
return_value=executable,
|
||||
),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.BUNDLED_GAMESCOPE_SHA256",
|
||||
"0" * 64,
|
||||
),
|
||||
):
|
||||
self.assertFalse(_bundled_gamescope_is_usable())
|
||||
|
||||
self.assertEqual(executable.stat().st_mode & 0o777, 0o644)
|
||||
|
||||
def test_selects_wayland_only_for_an_accessible_socket(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
socket_path = Path(temporary) / "wayland-test"
|
||||
with socket.socket(socket.AF_UNIX) as wayland_socket:
|
||||
wayland_socket.bind(str(socket_path))
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": socket_path.name,
|
||||
}
|
||||
),
|
||||
"wayland",
|
||||
)
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": socket_path.name,
|
||||
"DISPLAY": ":1",
|
||||
"XDG_CURRENT_DESKTOP": "X-Cinnamon",
|
||||
}
|
||||
),
|
||||
"sdl",
|
||||
)
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": socket_path.name,
|
||||
"XDG_CURRENT_DESKTOP": "X-Cinnamon",
|
||||
}
|
||||
),
|
||||
"wayland",
|
||||
)
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": socket_path.name,
|
||||
"DISPLAY": ":1",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
}
|
||||
),
|
||||
"sdl",
|
||||
)
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": socket_path.name,
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
}
|
||||
),
|
||||
"wayland",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
_select_host_backend(
|
||||
{
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
"WAYLAND_DISPLAY": "missing-wayland-socket",
|
||||
}
|
||||
),
|
||||
"sdl",
|
||||
)
|
||||
|
||||
def test_maps_signals_to_a_shell_exit_status(self) -> None:
|
||||
self.assertEqual(_process_exit_status(-6), 134)
|
||||
self.assertEqual(_process_exit_status(7), 7)
|
||||
|
||||
def test_tunes_resize_steps_and_keeps_the_host_window_composited(self) -> None:
|
||||
mapper = mock.Mock()
|
||||
mapper.find_window_by_pid.return_value = 41
|
||||
mapper.enable_window_scaling.return_value = True
|
||||
mapper.window_allows_scaling.return_value = True
|
||||
mapper.set_resize_increments.return_value = True
|
||||
mapper.prefer_composited_presentation.return_value = True
|
||||
process = mock.Mock()
|
||||
process.pid = 1234
|
||||
process.poll.return_value = None
|
||||
publisher = mock.Mock()
|
||||
|
||||
self.assertEqual(
|
||||
_tune_process_window(
|
||||
mapper,
|
||||
process,
|
||||
1280,
|
||||
960,
|
||||
host_focus_publisher=publisher,
|
||||
),
|
||||
(64, 48),
|
||||
)
|
||||
self.assertEqual(publisher.update.call_args_list, [
|
||||
mock.call(mapper, 41),
|
||||
mock.call(mapper, 41),
|
||||
])
|
||||
mapper.enable_window_scaling.assert_called_once_with(41, confirm=False)
|
||||
mapper.window_allows_scaling.assert_called_once_with(41)
|
||||
mapper.set_resize_increments.assert_called_once_with(
|
||||
41,
|
||||
(1280, 960),
|
||||
(64, 48),
|
||||
)
|
||||
mapper.prefer_composited_presentation.assert_called_once_with(41)
|
||||
|
||||
def test_tuning_retains_hints_while_muffin_refreshes_its_frame(self) -> None:
|
||||
mapper = mock.Mock()
|
||||
mapper.find_window_by_pid.return_value = 41
|
||||
mapper.enable_window_scaling.return_value = True
|
||||
mapper.window_allows_scaling.side_effect = [False, True]
|
||||
mapper.set_resize_increments.return_value = True
|
||||
mapper.prefer_composited_presentation.return_value = True
|
||||
process = mock.Mock(pid=1234)
|
||||
process.poll.return_value = None
|
||||
|
||||
with (
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.time.monotonic",
|
||||
side_effect=[0.0, 0.0, 0.1],
|
||||
),
|
||||
mock.patch("ptlinux.gamescope_runtime.time.sleep") as sleep,
|
||||
):
|
||||
self.assertEqual(
|
||||
_tune_process_window(mapper, process, 1280, 960),
|
||||
(64, 48),
|
||||
)
|
||||
|
||||
mapper.find_window_by_pid.assert_called_once_with(1234)
|
||||
self.assertEqual(
|
||||
mapper.enable_window_scaling.call_args_list,
|
||||
[
|
||||
mock.call(41, confirm=False),
|
||||
mock.call(41, confirm=False),
|
||||
],
|
||||
)
|
||||
sleep.assert_called_once_with(0.05)
|
||||
|
||||
def test_host_focus_publisher_tracks_active_and_minimized_state(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"XDG_RUNTIME_DIR": temporary}),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.time.monotonic",
|
||||
side_effect=[10.0, 10.1, 10.2],
|
||||
),
|
||||
):
|
||||
publisher = _HostFocusPublisher.create()
|
||||
try:
|
||||
mapper = mock.Mock()
|
||||
mapper.window_state.return_value = 1
|
||||
mapper.window_is_active.return_value = True
|
||||
publisher.update(mapper, 41)
|
||||
self.assertEqual(
|
||||
publisher.path.read_text(encoding="ascii").split()[0],
|
||||
"1",
|
||||
)
|
||||
|
||||
mapper.window_state.return_value = 3
|
||||
publisher.update(mapper, 41)
|
||||
self.assertEqual(
|
||||
publisher.path.read_text(encoding="ascii").split()[0],
|
||||
"0",
|
||||
)
|
||||
finally:
|
||||
state_path = publisher.path
|
||||
publisher.close()
|
||||
|
||||
self.assertFalse(state_path.exists())
|
||||
|
||||
def test_host_focus_publisher_preserves_an_inactive_minimized_remap(
|
||||
self,
|
||||
) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"XDG_RUNTIME_DIR": temporary}),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.time.monotonic",
|
||||
side_effect=[10.0, 10.1, 10.2, 10.3, 10.4],
|
||||
),
|
||||
):
|
||||
publisher = _HostFocusPublisher.create()
|
||||
try:
|
||||
mapper = mock.Mock()
|
||||
mapper.window_state.side_effect = [1, 3, 1, 1]
|
||||
mapper.window_is_active.side_effect = [True, False, True]
|
||||
|
||||
publisher.update(mapper, 41)
|
||||
publisher.update(mapper, 41)
|
||||
publisher.update(mapper, 41)
|
||||
|
||||
mapper.minimize_window.assert_called_once_with(41)
|
||||
self.assertEqual(
|
||||
publisher.path.read_text(encoding="ascii").split()[0],
|
||||
"0",
|
||||
)
|
||||
|
||||
publisher.update(mapper, 41)
|
||||
self.assertEqual(
|
||||
publisher.path.read_text(encoding="ascii").split()[0],
|
||||
"1",
|
||||
)
|
||||
mapper.minimize_window.assert_called_once_with(41)
|
||||
finally:
|
||||
publisher.close()
|
||||
|
||||
def test_host_focus_diagnostics_capture_the_minimize_remap_cycle(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
environment = {
|
||||
"XDG_RUNTIME_DIR": temporary,
|
||||
WINDOW_DIAGNOSTICS_ENV: "1",
|
||||
WINDOW_DIAGNOSTICS_SESSION_ENV: "b" * 32,
|
||||
}
|
||||
output = io.StringIO()
|
||||
with (
|
||||
mock.patch.dict(os.environ, environment, clear=True),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime.time.monotonic",
|
||||
side_effect=[10.0, 10.1, 10.2, 10.4],
|
||||
),
|
||||
contextlib.redirect_stderr(output),
|
||||
):
|
||||
diagnostics = WindowDiagnosticRecorder(
|
||||
"gamescope_host",
|
||||
environment=environment,
|
||||
monotonic=lambda: 20.0,
|
||||
)
|
||||
publisher = _HostFocusPublisher.create(diagnostics=diagnostics)
|
||||
summary: dict[str, object] = {}
|
||||
try:
|
||||
mapper = mock.Mock()
|
||||
mapper.window_state.side_effect = [1, 3, 1]
|
||||
mapper.window_is_active.side_effect = [True, False]
|
||||
mapper.minimize_window.return_value = True
|
||||
mapper.window_diagnostic_snapshot.return_value = {
|
||||
"wm_state": "normal",
|
||||
"allows_resize": True,
|
||||
"allows_maximize_horizontal": False,
|
||||
"allows_maximize_vertical": False,
|
||||
}
|
||||
mapper.window_net_wm_state_flags.side_effect = [
|
||||
{
|
||||
"hidden": False,
|
||||
"above": False,
|
||||
"demands_attention": False,
|
||||
"maximized_horizontal": False,
|
||||
"maximized_vertical": False,
|
||||
},
|
||||
{
|
||||
"hidden": True,
|
||||
"above": False,
|
||||
"demands_attention": False,
|
||||
"maximized_horizontal": False,
|
||||
"maximized_vertical": False,
|
||||
},
|
||||
{
|
||||
"hidden": False,
|
||||
"above": False,
|
||||
"demands_attention": False,
|
||||
"maximized_horizontal": False,
|
||||
"maximized_vertical": False,
|
||||
},
|
||||
]
|
||||
|
||||
publisher.update(mapper, 41)
|
||||
publisher.update(mapper, 41)
|
||||
publisher.update(mapper, 41)
|
||||
summary = publisher.summary_details()
|
||||
finally:
|
||||
publisher.close()
|
||||
diagnostics.close(**summary)
|
||||
|
||||
payloads = [
|
||||
parse_window_diagnostic(line)
|
||||
for line in output.getvalue().splitlines()
|
||||
]
|
||||
diagnostics_by_name = {
|
||||
payload["diagnostic"]: payload
|
||||
for payload in payloads
|
||||
if payload is not None
|
||||
}
|
||||
self.assertIn("host_minimize_latched", diagnostics_by_name)
|
||||
self.assertIn("host_hidden_state_cleared", diagnostics_by_name)
|
||||
remap = diagnostics_by_name["host_inactive_remap"]
|
||||
details = remap["details"]
|
||||
assert isinstance(details, dict)
|
||||
self.assertTrue(details["minimize_request_accepted"])
|
||||
self.assertFalse(details["allows_maximize_horizontal"])
|
||||
self.assertEqual(summary["inactive_remaps"], 1)
|
||||
self.assertEqual(summary["hidden_state_clears"], 1)
|
||||
self.assertEqual(summary["reiconify_accepted"], 1)
|
||||
|
||||
def test_optimized_wait_refreshes_host_focus_before_returning(self) -> None:
|
||||
process = mock.Mock(pid=1234)
|
||||
process.poll.return_value = 0
|
||||
mapper = mock.Mock()
|
||||
mapper.find_window_by_pid.return_value = 41
|
||||
publisher = mock.Mock()
|
||||
|
||||
self.assertEqual(
|
||||
_wait_for_optimized_gamescope(
|
||||
process,
|
||||
None,
|
||||
host_focus_publisher=publisher,
|
||||
host_mapper=mapper,
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
publisher.update.assert_called_once_with(mapper, 41)
|
||||
|
||||
def test_optimized_wait_returns_the_nested_child_status(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
status_file = Path(temporary) / "status"
|
||||
status_file.write_text("0\n", encoding="ascii")
|
||||
process = mock.Mock()
|
||||
process.poll.return_value = None
|
||||
|
||||
self.assertEqual(_wait_for_optimized_gamescope(process, status_file), 0)
|
||||
|
||||
process.terminate.assert_called_once_with()
|
||||
process.kill.assert_not_called()
|
||||
process.wait.assert_called_once_with(timeout=2.0)
|
||||
|
||||
@mock.patch("ptlinux.gamescope_runtime.subprocess.Popen")
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._tune_process_window",
|
||||
return_value=(64, 48),
|
||||
)
|
||||
@mock.patch("ptlinux.gamescope_runtime._open_host_x11")
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._select_host_backend",
|
||||
return_value="sdl",
|
||||
)
|
||||
def test_x11_path_tunes_the_window_and_preserves_child_status(
|
||||
self,
|
||||
_select_host_backend: mock.Mock,
|
||||
open_host_x11: mock.Mock,
|
||||
tune_process_window: mock.Mock,
|
||||
popen: mock.Mock,
|
||||
) -> None:
|
||||
mapper = open_host_x11.return_value
|
||||
process = popen.return_value
|
||||
process.poll.return_value = 7
|
||||
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"DISPLAY": ":0"}, clear=True),
|
||||
contextlib.redirect_stderr(stderr),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_child_status",
|
||||
return_value=7,
|
||||
),
|
||||
):
|
||||
status = run_gamescope(["gamescope", "--", "game"], 1280, 960)
|
||||
|
||||
self.assertEqual(status, 7)
|
||||
self.assertEqual(
|
||||
popen.call_args.args[0],
|
||||
[
|
||||
"gamescope",
|
||||
"--backend",
|
||||
"sdl",
|
||||
"--force-composition",
|
||||
"--",
|
||||
mock.ANY,
|
||||
"-P",
|
||||
"-m",
|
||||
"ptlinux.gamescope_child_runtime",
|
||||
"--status-file",
|
||||
mock.ANY,
|
||||
"--",
|
||||
"game",
|
||||
],
|
||||
)
|
||||
self.assertEqual(popen.call_args.kwargs["env"]["SDL_VIDEODRIVER"], "x11")
|
||||
self.assertEqual(
|
||||
popen.call_args.kwargs["env"][
|
||||
"SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR"
|
||||
],
|
||||
"0",
|
||||
)
|
||||
self.assertIn(
|
||||
GAMESCOPE_HOST_FOCUS_STATE_ENV,
|
||||
popen.call_args.kwargs["env"],
|
||||
)
|
||||
tune_process_window.assert_called_once_with(
|
||||
mapper,
|
||||
process,
|
||||
1280,
|
||||
960,
|
||||
host_focus_publisher=mock.ANY,
|
||||
)
|
||||
mapper.close.assert_called_once_with()
|
||||
self.assertIn("64x48", stderr.getvalue())
|
||||
|
||||
@mock.patch("ptlinux.gamescope_runtime.subprocess.Popen")
|
||||
@mock.patch("ptlinux.gamescope_runtime._open_host_x11", return_value=None)
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._select_host_backend",
|
||||
return_value="sdl",
|
||||
)
|
||||
def test_x11_path_disables_nested_focus_repair_without_host_monitor(
|
||||
self,
|
||||
_select_host_backend: mock.Mock,
|
||||
_open_host_x11: mock.Mock,
|
||||
popen: mock.Mock,
|
||||
) -> None:
|
||||
process = popen.return_value
|
||||
process.poll.return_value = 0
|
||||
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"DISPLAY": ":0"}, clear=True),
|
||||
contextlib.redirect_stderr(io.StringIO()),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_child_status",
|
||||
return_value=0,
|
||||
),
|
||||
):
|
||||
self.assertEqual(
|
||||
run_gamescope(["gamescope", "--", "game"], 1280, 960),
|
||||
0,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
popen.call_args.kwargs["env"][GAMESCOPE_HOST_FOCUS_STATE_ENV],
|
||||
"unavailable",
|
||||
)
|
||||
|
||||
@mock.patch("ptlinux.gamescope_runtime.subprocess.Popen")
|
||||
@mock.patch("ptlinux.gamescope_runtime._open_host_x11")
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._select_wayland_gamescope",
|
||||
return_value=("/opt/ptlinux/gamescope", False, True),
|
||||
)
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._select_host_backend",
|
||||
return_value="wayland",
|
||||
)
|
||||
def test_wayland_path_avoids_sdl_and_preserves_child_status(
|
||||
self,
|
||||
_select_host_backend: mock.Mock,
|
||||
select_wayland_gamescope: mock.Mock,
|
||||
open_host_x11: mock.Mock,
|
||||
popen: mock.Mock,
|
||||
) -> None:
|
||||
process = popen.return_value
|
||||
process.poll.return_value = 0
|
||||
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
mock.patch.dict(os.environ, {"WAYLAND_DISPLAY": "wayland-0"}, clear=True),
|
||||
contextlib.redirect_stderr(stderr),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_child_status",
|
||||
return_value=0,
|
||||
),
|
||||
):
|
||||
status = run_gamescope(["gamescope", "--", "game"], 1280, 960)
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
popen.call_args.args[0],
|
||||
[
|
||||
"/opt/ptlinux/gamescope",
|
||||
"--backend",
|
||||
"wayland",
|
||||
"--",
|
||||
mock.ANY,
|
||||
"-P",
|
||||
"-m",
|
||||
"ptlinux.gamescope_child_runtime",
|
||||
"--status-file",
|
||||
mock.ANY,
|
||||
"--",
|
||||
"game",
|
||||
],
|
||||
)
|
||||
self.assertNotIn("SDL_VIDEODRIVER", popen.call_args.kwargs["env"])
|
||||
self.assertNotIn(
|
||||
"SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR",
|
||||
popen.call_args.kwargs["env"],
|
||||
)
|
||||
self.assertEqual(
|
||||
popen.call_args.kwargs["env"][
|
||||
"PTLINUX_GAMESCOPE_KEEP_WINDOWS_VISIBLE"
|
||||
],
|
||||
"1",
|
||||
)
|
||||
open_host_x11.assert_not_called()
|
||||
select_wayland_gamescope.assert_called_once_with(
|
||||
"gamescope",
|
||||
environment=mock.ANY,
|
||||
)
|
||||
self.assertIn("native Wayland", stderr.getvalue())
|
||||
self.assertIn("lazy-output", stderr.getvalue())
|
||||
self.assertIn("login-window visibility", stderr.getvalue())
|
||||
|
||||
@mock.patch("ptlinux.gamescope_runtime.subprocess.Popen")
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._tune_process_window",
|
||||
return_value=(64, 48),
|
||||
)
|
||||
@mock.patch("ptlinux.gamescope_runtime._open_host_x11")
|
||||
@mock.patch(
|
||||
"ptlinux.gamescope_runtime._select_host_backend",
|
||||
return_value="sdl",
|
||||
)
|
||||
def test_kubuntu_wayland_uses_xwayland_presentation_and_reports_policy(
|
||||
self,
|
||||
_select_host_backend: mock.Mock,
|
||||
open_host_x11: mock.Mock,
|
||||
tune_process_window: mock.Mock,
|
||||
popen: mock.Mock,
|
||||
) -> None:
|
||||
process = popen.return_value
|
||||
process.poll.return_value = 0
|
||||
environment = {
|
||||
"WAYLAND_DISPLAY": "wayland-0",
|
||||
"DISPLAY": ":1",
|
||||
"XDG_CURRENT_DESKTOP": "KDE",
|
||||
"DESKTOP_SESSION": "plasmawayland",
|
||||
WINDOW_DIAGNOSTICS_ENV: "1",
|
||||
WINDOW_DIAGNOSTICS_SESSION_ENV: "c" * 32,
|
||||
}
|
||||
stderr = io.StringIO()
|
||||
with (
|
||||
mock.patch.dict(os.environ, environment, clear=True),
|
||||
mock.patch(
|
||||
"ptlinux.gamescope_runtime._read_child_status",
|
||||
return_value=0,
|
||||
),
|
||||
contextlib.redirect_stderr(stderr),
|
||||
):
|
||||
status = run_gamescope(["gamescope", "--", "game"], 1280, 960)
|
||||
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(
|
||||
popen.call_args.args[0][:4],
|
||||
[
|
||||
"gamescope",
|
||||
"--backend",
|
||||
"sdl",
|
||||
"--force-composition",
|
||||
],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"PTLINUX_GAMESCOPE_KEEP_WINDOWS_VISIBLE",
|
||||
popen.call_args.kwargs["env"],
|
||||
)
|
||||
open_host_x11.assert_called_once_with()
|
||||
tune_process_window.assert_called_once()
|
||||
self.assertIn("KDE Wayland compatibility mode active", stderr.getvalue())
|
||||
payloads = [
|
||||
parse_window_diagnostic(line)
|
||||
for line in stderr.getvalue().splitlines()
|
||||
]
|
||||
runtime_configured = next(
|
||||
payload
|
||||
for payload in payloads
|
||||
if payload is not None and payload["diagnostic"] == "runtime_configured"
|
||||
)
|
||||
details = runtime_configured["details"]
|
||||
assert isinstance(details, dict)
|
||||
self.assertTrue(details["kde_session"])
|
||||
self.assertTrue(details["kde_wayland_compatibility_mode"])
|
||||
self.assertTrue(details["kde_wayland_xwayland_fallback"])
|
||||
self.assertEqual(
|
||||
details["backend_reason"],
|
||||
"kde_wayland_xwayland_compatibility",
|
||||
)
|
||||
self.assertEqual(details["presentation_mode"], "sdl_composed")
|
||||
|
||||
def test_main_reports_an_empty_command(self) -> None:
|
||||
stderr = io.StringIO()
|
||||
with contextlib.redirect_stderr(stderr):
|
||||
status = main(["--width", "1280", "--height", "960"])
|
||||
|
||||
self.assertEqual(status, 1)
|
||||
self.assertIn("cannot be empty", stderr.getvalue())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user