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

215 lines
6.2 KiB
Python

from __future__ import annotations
import queue
import subprocess
import unittest
from unittest.mock import MagicMock
from ptlinux.ui.controllers.operation import start_operation_process
from ptlinux.ui.viewmodels.downloader import ProgressCoalescer
class _InlineThread:
def __init__(self, *, target, name: str, daemon: bool) -> None:
self._target = target
self.name = name
self.daemon = daemon
def start(self) -> None:
self._target()
class _Process:
def __init__(self, lines: list[str], returncode: int = 0, pid: int = 4321) -> None:
self.stdout = iter(lines)
self.returncode = returncode
self.pid = pid
def wait(self) -> int:
return self.returncode
def _drain(events: queue.Queue[tuple[str, object]]) -> list[tuple[str, object]]:
values: list[tuple[str, object]] = []
while not events.empty():
values.append(events.get_nowait())
return values
class DashboardOperationControllerTests(unittest.TestCase):
def test_routes_status_progress_output_and_completion(self) -> None:
events: queue.Queue[tuple[str, object]] = queue.Queue()
progress = ProgressCoalescer()
registry = MagicMock()
process = _Process(
[
'STATUS {"step": 1, "total": 3, "message": "Starting"}\n',
'PROGRESS {"phase": "downloading", "bytes_completed": 8}\n',
"plain output\n",
"STATUS invalid\n",
],
returncode=7,
)
seen_process_args: list[tuple[list[str], dict[str, object]]] = []
def process_factory(command: list[str], **kwargs: object) -> _Process:
seen_process_args.append((command, kwargs))
return process
start_operation_process(
["launcher", "update"],
events=events,
progress_updates=progress,
game_launch_supervisors=registry,
child_environment=lambda: {"ROLE": "operation"},
interrupt_supervisor=MagicMock(),
process_factory=process_factory,
thread_factory=_InlineThread,
)
self.assertEqual(
_drain(events),
[
(
"operation_status",
{"step": 1, "total": 3, "message": "Starting"},
),
("output", "plain output\n"),
("output", "STATUS invalid\n"),
("done", 7),
],
)
self.assertEqual(
progress.take(),
{"phase": "downloading", "bytes_completed": 8},
)
self.assertEqual(seen_process_args[0][0], ["launcher", "update"])
self.assertEqual(seen_process_args[0][1]["env"], {"ROLE": "operation"})
self.assertEqual(seen_process_args[0][1]["stdout"], subprocess.PIPE)
self.assertEqual(seen_process_args[0][1]["stderr"], subprocess.STDOUT)
def test_routes_game_readiness_and_cancellation_race(self) -> None:
events: queue.Queue[tuple[str, object]] = queue.Queue()
registry = MagicMock()
registry.is_cancel_requested.return_value = False
registry.attach.return_value = True
process = _Process(
[
"Game window created; preparing it\n",
"Game window ready: PristonTale EU\n",
],
)
interrupt = MagicMock(return_value=True)
start_operation_process(
["launcher", "play"],
events=events,
progress_updates=ProgressCoalescer(),
game_launch_supervisors=registry,
child_environment=dict,
interrupt_supervisor=interrupt,
game_client_id=3,
process_factory=lambda *_args, **_kwargs: process,
thread_factory=_InlineThread,
)
kinds = [kind for kind, _value in _drain(events)]
self.assertEqual(
kinds,
[
"game_supervisor_started",
"output",
"output",
"game_operation_status",
"game_started",
"output",
"game_window_ready",
"game_done",
],
)
registry.attach.assert_called_once_with(3, process)
registry.detach.assert_called_once_with(3, process)
interrupt.assert_called_once_with(process)
def test_unmonitored_process_releases_controls_without_a_window_ready_event(self) -> None:
for line in (
"Game window monitoring unavailable; process started: PristonTale EU\n",
"Game window ready: unmanaged PristonTale EU\n",
):
with self.subTest(line=line):
events: queue.Queue[tuple[str, object]] = queue.Queue()
registry = MagicMock()
registry.is_cancel_requested.return_value = False
registry.attach.return_value = False
process = _Process([line], returncode=1)
start_operation_process(
["launcher", "play"], events=events,
progress_updates=ProgressCoalescer(), game_launch_supervisors=registry,
child_environment=dict, interrupt_supervisor=MagicMock(), game_client_id=3,
process_factory=lambda *_args, **_kwargs: process, thread_factory=_InlineThread,
)
result = _drain(events)
self.assertIn(("game_window_unmonitored", 3), result)
self.assertNotIn(("game_window_ready", 3), result)
self.assertEqual(result[-1], ("game_done", (3, 1)))
def test_cancellation_before_process_start_skips_the_child(self) -> None:
events: queue.Queue[tuple[str, object]] = queue.Queue()
registry = MagicMock()
registry.is_cancel_requested.return_value = True
process_factory = MagicMock()
start_operation_process(
["launcher", "play"],
events=events,
progress_updates=ProgressCoalescer(),
game_launch_supervisors=registry,
child_environment=dict,
interrupt_supervisor=MagicMock(),
game_client_id=4,
process_factory=process_factory,
thread_factory=_InlineThread,
)
self.assertEqual(
_drain(events),
[
(
"output",
"[Client 4] Launch cancelled before the game supervisor started.\n",
),
("game_done", (4, 130)),
],
)
process_factory.assert_not_called()
def test_process_start_failure_uses_the_matching_completion_event(self) -> None:
for client_id, done_event in ((None, ("done", 1)), (5, ("game_done", (5, 1)))):
with self.subTest(client_id=client_id):
events: queue.Queue[tuple[str, object]] = queue.Queue()
registry = MagicMock()
registry.is_cancel_requested.return_value = False
def fail_to_start(*_args: object, **_kwargs: object) -> _Process:
raise OSError("missing executable")
start_operation_process(
["missing"],
events=events,
progress_updates=ProgressCoalescer(),
game_launch_supervisors=registry,
child_environment=dict,
interrupt_supervisor=MagicMock(),
game_client_id=client_id,
process_factory=fail_to_start,
thread_factory=_InlineThread,
)
emitted = _drain(events)
self.assertIn(done_event, emitted)
self.assertIn("Could not start operation", str(emitted[0][1]))
if __name__ == "__main__":
unittest.main()