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

111 lines
4.5 KiB
Python

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