Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
110
tests/test_gamescope_shutdown_regressions.py
Normal file
110
tests/test_gamescope_shutdown_regressions.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Exercise supervisor failure paths without launching a game or display server."""
|
||||
|
||||
from contextlib import redirect_stderr
|
||||
import io
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from ptlinux.presentation import gamescope
|
||||
from scripts.repository_paths import SOURCE_ROOT
|
||||
|
||||
|
||||
class GamescopeShutdownRegressions(unittest.TestCase):
|
||||
def test_missing_status_storage_cannot_be_reported_as_a_clean_game_exit(self) -> None:
|
||||
process = MagicMock()
|
||||
process.poll.return_value = 0
|
||||
output = io.StringIO()
|
||||
with (
|
||||
patch.object(gamescope, "_select_host_backend", return_value="sdl"),
|
||||
patch.object(gamescope, "_open_host_x11", return_value=None),
|
||||
patch.object(gamescope, "_create_child_status_file", side_effect=OSError("no space left")),
|
||||
patch.object(gamescope.subprocess, "Popen", return_value=process) as popen,
|
||||
redirect_stderr(output),
|
||||
):
|
||||
self.assertEqual(gamescope.run_gamescope(["gamescope", "--", "game"], 1280, 960), 1)
|
||||
self.assertNotIn("--keep-alive", popen.call_args.args[0])
|
||||
self.assertIn("supervision is unavailable", output.getvalue())
|
||||
self.assertIn("without a recorded game status (compositor status 0)", output.getvalue())
|
||||
|
||||
def test_interrupt_during_startup_tuning_closes_the_compositor_and_state_files(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
mapper = MagicMock()
|
||||
process = MagicMock()
|
||||
process.poll.return_value = None
|
||||
process.wait.return_value = -15
|
||||
with (
|
||||
patch.dict(os.environ, {"DISPLAY": ":0", "XDG_RUNTIME_DIR": temporary}, clear=True),
|
||||
patch.object(gamescope, "_select_host_backend", return_value="sdl"),
|
||||
patch.object(gamescope, "_open_host_x11", return_value=mapper),
|
||||
patch.object(gamescope, "_tune_process_window", side_effect=KeyboardInterrupt),
|
||||
patch.object(gamescope.subprocess, "Popen", return_value=process),
|
||||
self.assertRaises(KeyboardInterrupt),
|
||||
):
|
||||
gamescope.run_gamescope(["gamescope", "--", "game"], 1280, 960)
|
||||
process.terminate.assert_called_once_with()
|
||||
process.wait.assert_called_once_with(timeout=gamescope.GAMESCOPE_SHUTDOWN_TIMEOUT_SECONDS)
|
||||
mapper.close.assert_called_once_with()
|
||||
self.assertEqual(list(Path(temporary).iterdir()), [])
|
||||
|
||||
def test_child_report_failure_does_not_leave_an_empty_compositor_alive(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
fake_gamescope = root / "gamescope"
|
||||
fake_gamescope.write_text(
|
||||
f"#!{sys.executable}\n" + textwrap.dedent("""\
|
||||
import subprocess, sys, time
|
||||
separator = sys.argv.index('--')
|
||||
subprocess.run(sys.argv[separator + 1:], check=False)
|
||||
# Gamescope's default exits when the primary child finishes;
|
||||
# --keep-alive disables that behavior, even if the child failed.
|
||||
if '--keep-alive' in sys.argv[1:separator]:
|
||||
time.sleep(15)
|
||||
"""), encoding="utf-8",
|
||||
)
|
||||
fake_gamescope.chmod(0o700)
|
||||
runner = root / "supervisor.py"
|
||||
runner.write_text(textwrap.dedent("""\
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
from ptlinux.presentation import gamescope
|
||||
status = Path(sys.argv[2])
|
||||
with (
|
||||
patch.object(gamescope, '_select_host_backend', return_value='wayland'),
|
||||
patch.object(gamescope, '_select_wayland_gamescope', return_value=(sys.argv[1], True, False)),
|
||||
patch.object(gamescope, '_create_child_status_file', return_value=status),
|
||||
):
|
||||
raise SystemExit(gamescope.run_gamescope(
|
||||
[sys.argv[1], '--', sys.executable, '-c', 'raise SystemExit(7)'], 1280, 960,
|
||||
))
|
||||
"""), encoding="utf-8")
|
||||
for status, expected in ((root / "status", 7), (root / "removed-directory" / "status", 1)):
|
||||
with self.subTest(status=status):
|
||||
process = subprocess.Popen(
|
||||
[sys.executable, str(runner), str(fake_gamescope), str(status)],
|
||||
env={**os.environ, "PYTHONPATH": str(SOURCE_ROOT)},
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True,
|
||||
)
|
||||
try:
|
||||
_stdout, stderr = process.communicate(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
process.communicate(timeout=5)
|
||||
raise
|
||||
self.assertEqual(process.returncode, expected, stderr)
|
||||
if expected == 1:
|
||||
self.assertIn("Could not report the nested game status", stderr)
|
||||
self.assertIn("without a recorded game status", stderr)
|
||||
else:
|
||||
self.assertNotIn("without a recorded game status", stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user