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

1426 lines
43 KiB
Python

import contextlib
import io
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import MagicMock, patch
from ptlinux.cli import (
_configure_launcher_runtime,
_configured_download_options,
_launcher_update,
_native_update,
_doctor,
_current_game_crash,
_ensure_portable_umu_for_play,
_game_exit_metadata,
_play,
_play_locked,
_print_game_outcome,
_print_interrupted_game_log,
_runtime_issue_summary,
_source_game,
build_parser,
main,
)
from ptlinux.command import CommandOutcome
from ptlinux.application.commands.play import (
game_log_telemetry_summary,
recommended_runtime_comparison,
)
from ptlinux.depot import (
DepotIntegrityIssue,
DepotPreflightResult,
DepotSnapshot,
DepotStartOption,
DepotSyncResult,
cache_depot_launch_catalog,
)
from ptlinux.launcher import LauncherError
from ptlinux.doctor import ToolStatus
from ptlinux.paths import AppPaths
from ptlinux.runtime import RuntimeErrorPT, RuntimeSelection
from ptlinux.self_update import LauncherUpdateResult
from ptlinux.settings import Settings
from ptlinux.umu import UmuBootstrapError, UmuBootstrapResult
from ptlinux.window_diagnostics import (
WINDOW_DIAGNOSTIC_PREFIX,
WINDOW_DIAGNOSTICS_SESSION_ENV,
)
class CliInterruptTests(unittest.TestCase):
def test_doctor_separates_optional_tools_from_launch_blockers(self) -> None:
report = MagicMock(
ok=False,
distribution="linuxmint",
version="22",
architecture="x86_64",
display_backend="x11",
tools=[
ToolStatus("python-tk", None, True, "native dashboard"),
ToolStatus("gamescope", None, False, "optional scaling"),
],
blockers=("python-tk is missing (native dashboard).",),
warnings=[],
package_hint="Install Python/Tk with APT.",
)
stdout = io.StringIO()
with (
patch("ptlinux.cli.inspect_host", return_value=report),
contextlib.redirect_stdout(stdout),
):
returncode = _doctor(False)
output = stdout.getvalue()
self.assertEqual(returncode, 1)
self.assertIn(
"Optional tools missing (these do not block launch): gamescope",
output,
)
self.assertIn("Host readiness: BLOCKED", output)
self.assertIn("Blocking diagnostics:", output)
self.assertIn("python-tk is missing (native dashboard).", output)
def test_interprets_umu_status_247_as_wrapped_sigkill(self) -> None:
self.assertEqual(
_game_exit_metadata(247, "umu"),
{
"exit_code": 137,
"raw_exit_code": 247,
"signal": 9,
"signal_name": "SIGKILL",
"signal_inferred": True,
"signal_inference": "umu_python_system_exit",
},
)
def test_interprets_direct_proton_status_247_as_wrapped_sigkill(self) -> None:
self.assertEqual(
_game_exit_metadata(247, "proton7"),
{
"exit_code": 137,
"raw_exit_code": 247,
"signal": 9,
"signal_name": "SIGKILL",
"signal_inferred": True,
"signal_inference": "proton_python_system_exit",
},
)
def test_launcher_update_accepts_a_non_installing_full_verification(self) -> None:
arguments = build_parser().parse_args(["launcher-update", "--verify"])
self.assertTrue(arguments.verify)
def test_runtime_bootstrap_has_a_dedicated_install_command(self) -> None:
arguments = build_parser().parse_args(["runtime-bootstrap"])
self.assertEqual(arguments.command, "runtime-bootstrap")
def test_container_host_has_an_explicit_repair_command(self) -> None:
arguments = build_parser().parse_args(["container-host", "--repair"])
self.assertEqual(arguments.command, "container-host")
self.assertTrue(arguments.repair)
def test_forced_runtime_setup_emits_recovery_request_and_outcome(self) -> None:
telemetry = MagicMock()
runner = MagicMock(dry_run=False)
with (
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch("ptlinux.cli.configure_launcher_prefix", return_value=[]) as configure,
):
result = _configure_launcher_runtime(
MagicMock(),
MagicMock(),
runner,
force_runtime=True,
)
self.assertEqual(result, [])
configure.assert_called_once()
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertEqual(
events,
[
"runtime_reinstall_requested",
"runtime_repair_started",
"runtime_repair_success",
],
)
def test_explicit_game_repair_emits_requested_started_and_success(self) -> None:
telemetry = MagicMock()
paths = MagicMock()
paths.source_game.exists.return_value = True
with (
patch("ptlinux.cli.AppPaths.from_environment", return_value=paths),
patch(
"ptlinux.cli.initialize_shared_telemetry",
return_value=(telemetry, False),
) as initialize_telemetry,
patch("ptlinux.cli._dispatch_command", return_value=0),
):
returncode = main(
["sync", "--repair"],
_manage_telemetry=True,
)
self.assertEqual(returncode, 0)
self.assertFalse(
initialize_telemetry.call_args.kwargs["report_launcher_lifecycle"]
)
telemetry.mark_ready.assert_not_called()
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertEqual(
events,
["repair_requested", "repair_started", "repair_success"],
)
def test_forced_runtime_setup_emits_failed_recovery_outcome(self) -> None:
telemetry = MagicMock()
runner = MagicMock(dry_run=False)
with (
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch(
"ptlinux.cli.configure_launcher_prefix",
side_effect=LauncherError("runtime setup failed"),
),
self.assertRaisesRegex(LauncherError, "runtime setup failed"),
):
_configure_launcher_runtime(
MagicMock(),
MagicMock(),
runner,
force_runtime=True,
)
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertEqual(events[-1], "runtime_repair_failed")
def test_launcher_update_still_restarts_when_umu_must_retry(self) -> None:
update = LauncherUpdateResult(
changed=True,
current_version="0.8.3",
installed_version="0.8.4",
release_directory="/release/0.8.4",
previous_release="/release/0.8.3",
archive="/cache/update.tar.gz",
archive_sha256="a" * 64,
restart_required=True,
state_recorded=True,
warning=None,
)
stderr = io.StringIO()
container_host = MagicMock(
ready=True,
broken_umu_profile_link=False,
repair_error=None,
recommendation=None,
)
container_host.to_dict.return_value = {"ready": True}
with (
patch("ptlinux.cli.install_launcher_update", return_value=update),
patch(
"ptlinux.cli.provision_umu",
side_effect=UmuBootstrapError("network unavailable"),
),
patch("ptlinux.cli.find_umu_launcher", return_value=None),
patch("ptlinux.cli.repair_container_host", return_value=container_host),
patch("ptlinux.cli._print_operation_status"),
patch("ptlinux.cli._emit") as emit,
contextlib.redirect_stderr(stderr),
):
returncode = _launcher_update(
MagicMock(),
False,
False,
False,
None,
None,
False,
)
self.assertEqual(returncode, 0)
self.assertIn("network unavailable", stderr.getvalue())
payload = emit.call_args.args[0]
self.assertTrue(payload["ok"])
self.assertIn("network unavailable", payload["runtime_bootstrap"]["warning"])
def test_login_diagnostics_accepts_a_read_only_runtime_override(self) -> None:
arguments = build_parser().parse_args(
["login-diagnostics", "--runtime", "proton7"]
)
self.assertEqual(arguments.runtime, "proton7")
def test_play_accepts_an_official_realm_name(self) -> None:
arguments = build_parser().parse_args(["play", "--realm", "Seasonal"])
self.assertEqual(arguments.realm, "Seasonal")
def test_keyboard_interrupt_returns_130_without_a_traceback(self) -> None:
stderr = io.StringIO()
with (
patch("ptlinux.cli._play", side_effect=KeyboardInterrupt),
contextlib.redirect_stderr(stderr),
):
returncode = main(["play"])
self.assertEqual(returncode, 130)
self.assertIn("compatibility runtime were stopped", stderr.getvalue())
self.assertNotIn("Traceback", stderr.getvalue())
def test_json_interrupt_is_machine_readable(self) -> None:
stdout = io.StringIO()
with (
patch("ptlinux.cli._play", side_effect=KeyboardInterrupt),
contextlib.redirect_stdout(stdout),
):
returncode = main(["--json", "play"])
self.assertEqual(returncode, 130)
self.assertEqual(json.loads(stdout.getvalue())["interrupted"], True)
def test_play_preflight_interrupt_is_telemetry_cancellation(self) -> None:
telemetry = MagicMock()
stderr = io.StringIO()
with (
patch(
"ptlinux.cli.initialize_shared_telemetry",
return_value=(telemetry, False),
),
patch("ptlinux.cli._play", side_effect=KeyboardInterrupt),
contextlib.redirect_stderr(stderr),
):
returncode = main(["play"], _manage_telemetry=True)
self.assertEqual(returncode, 130)
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertEqual(events, ["game_launch_cancelled"])
cancelled = telemetry.send_event.call_args
self.assertEqual(cancelled.kwargs["severity"], "info")
self.assertTrue(cancelled.kwargs["metadata"]["interrupted"])
self.assertEqual(cancelled.kwargs["metadata"]["launcher_exit_code"], 130)
self.assertEqual(
cancelled.kwargs["context"],
{"operation": "launch_game", "stage": "preflight"},
)
def test_genuine_play_failure_keeps_its_runtime_stage(self) -> None:
telemetry = MagicMock()
stderr = io.StringIO()
with (
patch(
"ptlinux.cli.initialize_shared_telemetry",
return_value=(telemetry, False),
),
patch(
"ptlinux.cli._play",
side_effect=RuntimeErrorPT("Proton runtime is unavailable"),
),
contextlib.redirect_stderr(stderr),
):
returncode = main(["play"], _manage_telemetry=True)
self.assertEqual(returncode, 1)
failure = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_launch_failed"
)
self.assertEqual(failure.kwargs["severity"], "error")
self.assertEqual(
failure.kwargs["context"],
{"operation": "launch_game", "stage": "runtime_check"},
)
def test_launcher_update_writable_release_uses_filesystem_stage(self) -> None:
telemetry = MagicMock()
stderr = io.StringIO()
with (
patch(
"ptlinux.cli.initialize_shared_telemetry",
return_value=(telemetry, False),
),
patch(
"ptlinux.cli._launcher_update",
side_effect=RuntimeError(
"launcher release path is group/world writable: release/__pycache__"
),
),
contextlib.redirect_stderr(stderr),
):
returncode = main(["launcher-update"], _manage_telemetry=True)
self.assertEqual(returncode, 1)
failure = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "update_failed"
)
self.assertEqual(
failure.kwargs["context"],
{"operation": "launcher_update", "stage": "filesystem"},
)
class CliGameSourceTests(unittest.TestCase):
def test_persisted_import_source_does_not_redirect_play(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
settings = Settings(game_source=str(root / "publisher-game"))
self.assertEqual(_source_game(paths, settings, None), paths.source_game)
def test_explicit_diagnostic_game_directory_still_takes_precedence(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
explicit = root / "diagnostic-game"
self.assertEqual(
_source_game(paths, Settings(), explicit),
explicit.resolve(),
)
class CliDownloadSettingsTests(unittest.TestCase):
def test_default_download_settings_need_no_override(self) -> None:
self.assertIsNone(_configured_download_options(Settings()))
def test_saved_download_controls_are_forwarded_to_the_depot(self) -> None:
options = _configured_download_options(
Settings(
download_speed_limit_mbps=10.0,
low_bandwidth_mode=True,
)
)
self.assertIsNotNone(options)
assert options is not None
self.assertEqual(options.bandwidth_limit_bytes_per_second, 10_000_000)
self.assertEqual(options.effective_bandwidth_limit, 5_000_000)
self.assertTrue(options.low_bandwidth)
class CliPlayLockTests(unittest.TestCase):
def test_play_passes_its_preparation_lease_to_the_game_launch(self) -> None:
paths = MagicMock()
lease = MagicMock()
lease.preparation_allowed = True
guard = MagicMock()
guard.__enter__.return_value = lease
guard.__exit__.return_value = False
with (
patch("ptlinux.cli._play_guard", return_value=guard) as play_guard,
patch("ptlinux.cli._play_locked", return_value=7) as play_locked,
):
result = _play(
paths,
None,
False,
False,
False,
False,
)
play_guard.assert_called_once_with(paths, False)
guard.__enter__.assert_called_once_with()
guard.__exit__.assert_called_once()
play_locked.assert_called_once_with(
paths,
None,
False,
False,
False,
False,
realm=None,
reuse_prepared=False,
before_game=lease.allow_shared_clients,
game_lease_fds=lease.inherited_fds,
)
self.assertEqual(result, 7)
def test_running_game_interrupt_emits_one_cancellation(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
selection = RuntimeSelection("umu", "umu-run", None, None)
telemetry = MagicMock()
def interrupt_after_window_ready(
*_args: object,
**kwargs: object,
) -> CommandOutcome:
output_filter = kwargs["output_line_filter"]
assert callable(output_filter)
output_filter("Game window ready: PristonTale EU\n")
raise KeyboardInterrupt
def play_until_interrupted(*_args: object, **_kwargs: object) -> int:
return _play_locked(
paths,
None,
False,
False,
False,
False,
reuse_prepared=True,
)
stderr = io.StringIO()
with (
patch("ptlinux.cli.AppPaths.from_environment", return_value=paths),
patch(
"ptlinux.cli.initialize_shared_telemetry",
return_value=(telemetry, False),
),
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch("ptlinux.cli._play", side_effect=play_until_interrupted),
patch(
"ptlinux.cli.game_import_requires_verification",
return_value=False,
),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli.select_runtime", return_value=selection),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.game_runtime_ready", return_value=True),
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", side_effect=interrupt_after_window_ready),
patch("ptlinux.cli._current_game_log", return_value=None),
patch("ptlinux.cli._print_operation_status"),
contextlib.redirect_stderr(stderr),
):
returncode = main(["play"], _manage_telemetry=True)
self.assertEqual(returncode, 130)
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertEqual(events.count("game_launch_cancelled"), 1)
self.assertNotIn("game_launch_failed", events)
self.assertNotIn("game_exited", events)
cancelled = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_launch_cancelled"
)
self.assertEqual(cancelled.kwargs["severity"], "info")
self.assertEqual(cancelled.kwargs["context"]["stage"], "running")
self.assertTrue(cancelled.kwargs["metadata"]["launch_succeeded"])
self.assertTrue(cancelled.kwargs["metadata"]["game_window_ready"])
self.assertEqual(
cancelled.kwargs["metadata"]["readiness_signal"],
"window_ready",
)
def test_play_resolves_ptr_babel_from_the_official_catalog(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
cache_depot_launch_catalog(
paths.depot_launch_catalog_cache,
DepotSnapshot(
region="Europe",
server="cdn.example/pristontale-eu",
name="PristonTale EU",
build=10,
version="10.0.0",
files=(),
deleted_files=(),
create_if_missing=(),
start_options=(
DepotStartOption("Babel", "Game.exe", "PTEU_REBORN"),
DepotStartOption(
"Test Babel",
"GameSe.exe",
"PTEU_REBORN",
),
),
default_start_option="Babel",
),
)
selection = RuntimeSelection("umu", "umu-run", None, None)
outcome = CommandOutcome([], 0, "", "", 0.0, False, False)
with (
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli.prepare_runtime_view", return_value=None),
patch(
"ptlinux.cli._ensure_portable_umu_for_play",
return_value=None,
) as ensure_umu,
patch(
"ptlinux.cli.select_runtime",
return_value=selection,
) as select_runtime_mock,
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.prepare_game_runtime"),
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", return_value=outcome) as run_game,
patch("ptlinux.cli._current_game_log", return_value=None),
patch("ptlinux.cli._emit"),
):
result = _play_locked(
paths,
None,
False,
False,
False,
False,
realm="PTR Babel",
)
self.assertEqual(result, 0)
self.assertEqual(
run_game.call_args.kwargs["start_option"],
DepotStartOption("PTR Babel", "GameSe.exe", "PTEU_REBORN"),
)
effective_settings = run_game.call_args.args[1]
self.assertEqual(effective_settings.realm, "PTR Babel")
self.assertEqual(effective_settings.runtime, "proton7")
ensure_umu.assert_called_once_with(
paths,
effective_settings,
dry_run=False,
)
self.assertEqual(select_runtime_mock.call_args.args[0], effective_settings)
self.assertFalse(run_game.call_args.kwargs["check"])
def test_additional_client_reuses_runtime_without_mutating_it(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
selection = RuntimeSelection("umu", "umu-run", None, None)
outcome = CommandOutcome([], 0, "", "", 0.0, False, False)
launch_order: list[str] = []
def before_game() -> None:
launch_order.append("shared lease")
def run_game(*_args: object, **_kwargs: object) -> CommandOutcome:
_kwargs["before_start"]()
launch_order.append("game")
return outcome
with (
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli._ensure_portable_umu_for_play", return_value=None),
patch("ptlinux.cli.select_runtime", return_value=selection),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.game_runtime_ready", return_value=True),
patch("ptlinux.cli.prepare_runtime_view") as prepare_view,
patch("ptlinux.cli.prepare_game_runtime") as prepare_runtime,
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", side_effect=run_game) as run_game_mock,
patch("ptlinux.cli._current_game_log", return_value=None),
patch("ptlinux.cli._emit"),
):
result = _play_locked(
paths,
None,
False,
False,
False,
False,
reuse_prepared=True,
before_game=before_game,
game_lease_fds=(17,),
)
prepare_view.assert_not_called()
prepare_runtime.assert_not_called()
self.assertEqual(launch_order, ["shared lease", "game"])
self.assertTrue(
run_game_mock.call_args.kwargs["concurrent_prefix_launch"]
)
self.assertEqual(run_game_mock.call_args.kwargs["pass_fds"], (17,))
self.assertEqual(result, 0)
def test_live_ready_window_survives_truncated_nonzero_process_output(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
selection = RuntimeSelection("umu", "umu-run", None, None)
outcome = CommandOutcome(
[],
247,
"",
"GStreamer-WARNING: final retained output after the readiness marker\n",
221.0,
False,
False,
)
telemetry = MagicMock()
events_before_process_exit: list[str] = []
def run_game_with_ready_signal(
*_args: object,
**kwargs: object,
) -> CommandOutcome:
output_filter = kwargs["output_line_filter"]
assert callable(output_filter)
extra_environment = kwargs["extra_environment"]
assert isinstance(extra_environment, dict)
diagnostic_session = extra_environment[
WINDOW_DIAGNOSTICS_SESSION_ENV
]
diagnostic_line = WINDOW_DIAGNOSTIC_PREFIX + json.dumps(
{
"schema": 1,
"component": "gamescope_host",
"diagnostic": "host_inactive_remap",
"sequence": 3,
"elapsed_ms": 1200,
"session_id": diagnostic_session,
"details": {
"wm_state": "normal",
"minimize_request_accepted": True,
},
}
) + "\n"
spoofed_diagnostic_line = diagnostic_line.replace(
diagnostic_session,
"0" * 32,
)
self.assertEqual(
output_filter(spoofed_diagnostic_line),
spoofed_diagnostic_line,
)
self.assertIsNone(output_filter(diagnostic_line))
self.assertEqual(
output_filter(
"vulkan: physical device queue doesn't support "
"presenting on our surface\n"
),
"vulkan: physical device queue doesn't support "
"presenting on our surface\n",
)
output_filter(
"vulkan: physical device queue doesn't support "
"presenting on our surface\n"
)
self.assertEqual(
output_filter("Game window ready: PristonTale EU\n"),
"Game window ready: PristonTale EU\n",
)
output_filter(
"Game window resizing is disabled to prevent repeated "
"Direct3D device resets.\n"
)
events_before_process_exit.extend(
call.args[0] for call in telemetry.send_event.call_args_list
)
return outcome
with (
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli.select_runtime", return_value=selection),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.game_runtime_ready", return_value=True),
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", side_effect=run_game_with_ready_signal),
patch("ptlinux.cli._current_game_log", return_value=None),
patch("ptlinux.cli._current_game_crash", return_value=None),
patch("ptlinux.cli._emit"),
):
result = _play_locked(
paths,
None,
False,
False,
False,
True,
reuse_prepared=True,
)
diagnostic_files = list(paths.logs.glob("windows-*.jsonl"))
self.assertEqual(len(diagnostic_files), 1)
diagnostics = [json.loads(line) for line in diagnostic_files[0].read_text().splitlines()]
self.assertEqual(len(diagnostics), 1)
self.assertEqual(diagnostics[0]["diagnostic"], "host_inactive_remap")
self.assertNotEqual(diagnostics[0]["session_id"], "0" * 32)
self.assertEqual(result, 1)
events = [call.args[0] for call in telemetry.send_event.call_args_list]
self.assertIn("game_launch_success", events_before_process_exit)
self.assertIn("game_window_diagnostic", events_before_process_exit)
self.assertNotIn("game_exited", events_before_process_exit)
self.assertIn("game_launch_success", events)
self.assertIn("game_exited", events)
self.assertNotIn("game_launch_failed", events)
self.assertEqual(events.count("game_launch_success"), 1)
self.assertEqual(events.count("game_window_diagnostic"), 2)
self.assertLess(events.index("game_launch_success"), events.index("game_exited"))
launch_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_launch_success"
)
self.assertEqual(launch_call.kwargs["context"]["stage"], "ready")
self.assertEqual(
launch_call.kwargs["metadata"]["readiness_signal"],
"window_ready",
)
exit_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_exited"
)
self.assertEqual(exit_call.kwargs["severity"], "warning")
self.assertEqual(
exit_call.kwargs["message"],
"Game session ended with status 247, consistent with SIGKILL wrapped by UMU",
)
self.assertEqual(exit_call.kwargs["metadata"]["exit_code"], 137)
self.assertEqual(exit_call.kwargs["metadata"]["raw_exit_code"], 247)
self.assertEqual(exit_call.kwargs["metadata"]["signal"], 9)
self.assertTrue(exit_call.kwargs["metadata"]["game_window_ready"])
self.assertEqual(
exit_call.kwargs["metadata"]["readiness_signal"],
"window_ready",
)
self.assertNotIn(
"Game window resizing",
exit_call.kwargs["metadata"]["stderr"],
)
diagnostic_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_window_diagnostic"
and call.kwargs["metadata"]["diagnostic"] == "host_inactive_remap"
)
self.assertEqual(diagnostic_call.kwargs["severity"], "warning")
self.assertEqual(
diagnostic_call.kwargs["metadata"]["diagnostic"],
"host_inactive_remap",
)
self.assertEqual(
diagnostic_call.kwargs["metadata"]["session_id"],
exit_call.kwargs["metadata"]["launch_session_id"],
)
presentation_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_window_diagnostic"
and call.kwargs["metadata"]["diagnostic"]
== "gamescope_present_queue_unsupported"
)
self.assertEqual(presentation_call.kwargs["severity"], "warning")
self.assertEqual(
presentation_call.kwargs["context"]["stage"],
"presentation",
)
def test_new_crash_dump_makes_a_zero_status_launch_fail(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
selection = RuntimeSelection("umu", "umu-run", None, None)
start_option = DepotStartOption("Babel", "Game.exe", "PTEU_REBORN")
outcome = CommandOutcome([], 0, "", "", 1.0, False, False)
crash = {
"path": str(root / "crash" / "crash_game.dat"),
"modified_at": "2026-09-05T11:07:07+02:00",
"size": 231_320,
}
native_log = {
"path": str(root / "logs" / "log_current.txt"),
"size": 4096,
"runtime_out_of_memory": True,
"out_of_memory_count": 2,
"out_of_memory_effect_paths": [
r"game\scripts\shaders\UI\Minimap.fx",
],
}
crash_attachment = {
"schema": 1,
"kind": "windows_minidump",
"encoding": "gzip+base64url",
"original_size": 231_320,
"compressed_size": 24_000,
"sha256": "a" * 64,
"data_base64url": "A" * 32_000,
}
telemetry = MagicMock()
telemetry.enabled = True
with (
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch(
"ptlinux.cli._load_or_default_settings",
return_value=Settings(game_crash_dump_upload_enabled=True),
),
patch("ptlinux.cli.select_depot_start_option", return_value=start_option),
patch("ptlinux.cli.read_current_depot_file_catalog", return_value=None),
patch("ptlinux.cli.select_runtime", return_value=selection),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.game_runtime_ready", return_value=True),
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", return_value=outcome),
patch("ptlinux.cli._current_game_log", return_value=native_log),
patch("ptlinux.cli._current_game_crash", return_value=crash),
patch(
"ptlinux.application.commands.play.prepare_minidump_attachment",
return_value=("included", crash_attachment),
),
patch("ptlinux.cli._emit") as emit,
):
result = _play_locked(
paths,
None,
False,
False,
False,
True,
reuse_prepared=True,
)
self.assertEqual(result, 1)
payload = emit.call_args.args[0]
self.assertFalse(payload["ok"])
self.assertEqual(payload["current_game_crash"], crash)
exit_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_exited"
)
self.assertEqual(
exit_call.kwargs["metadata"]["crash_dump_info"],
{
"modified_at": "2026-09-05T11:07:07+02:00",
"size": 231_320,
},
)
self.assertEqual(
exit_call.kwargs["metadata"]["failure_reason"],
"direct3d_resource_exhaustion",
)
self.assertEqual(
exit_call.kwargs["metadata"]["crash_dump_upload_status"],
"included",
)
self.assertEqual(exit_call.kwargs["attachment"], crash_attachment)
self.assertEqual(
exit_call.kwargs["message"],
"Game client logged Direct3D resource exhaustion and wrote a crash dump",
)
self.assertEqual(
exit_call.kwargs["metadata"]["native_game_log"],
{
"size": 4096,
"runtime_out_of_memory": True,
"out_of_memory_count": 2,
"out_of_memory_effect_paths": [
r"game\scripts\shaders\UI\Minimap.fx",
],
},
)
def test_zero_status_resource_exhaustion_is_reported_as_degraded(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
paths.runtime_view.mkdir(parents=True)
selection = RuntimeSelection("umu", "umu-run", None, None)
start_option = DepotStartOption("Babel", "Game.exe", "PTEU_REBORN")
outcome = CommandOutcome([], 0, "", "", 1.0, False, False)
native_log = {
"path": str(root / "logs" / "log_current.txt"),
"size": 4096,
"runtime_out_of_memory": True,
"out_of_memory_count": 4,
"out_of_memory_effect_paths": [
r"game\scripts\shaders\UI\Minimap.fx",
],
}
telemetry = MagicMock()
with (
patch("ptlinux.cli.current_telemetry", return_value=telemetry),
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli.select_depot_start_option", return_value=start_option),
patch("ptlinux.cli.read_current_depot_file_catalog", return_value=None),
patch("ptlinux.cli.select_runtime", return_value=selection),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli.game_runtime_ready", return_value=True),
patch("ptlinux.cli.snapshot_native_logs", return_value={}),
patch("ptlinux.cli.run_game", return_value=outcome),
patch("ptlinux.cli._current_game_log", return_value=native_log),
patch("ptlinux.cli._current_game_crash", return_value=None),
patch("ptlinux.cli._emit") as emit,
):
result = _play_locked(
paths,
None,
False,
False,
False,
True,
reuse_prepared=True,
)
self.assertEqual(result, 0)
payload = emit.call_args.args[0]
self.assertTrue(payload["ok"])
self.assertTrue(payload["degraded"])
self.assertEqual(
payload["session_issue"],
"direct3d_resource_exhaustion",
)
exit_call = next(
call
for call in telemetry.send_event.call_args_list
if call.args[0] == "game_exited"
)
self.assertEqual(exit_call.kwargs["severity"], "warning")
self.assertEqual(
exit_call.kwargs["message"],
"Game client exited after logging Direct3D resource exhaustion",
)
self.assertTrue(exit_call.kwargs["metadata"]["clean_exit"])
self.assertTrue(exit_call.kwargs["metadata"]["degraded"])
self.assertEqual(
exit_call.kwargs["metadata"]["failure_reason"],
"direct3d_resource_exhaustion",
)
self.assertEqual(
exit_call.kwargs["metadata"]["native_game_log"]["out_of_memory_count"],
4,
)
def test_managed_play_stops_before_prepare_when_preflight_finds_damage(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
catalog = MagicMock(files=())
preflight = DepotPreflightResult(
build=10,
files_total=1,
verified_files=0,
unverified_files=0,
changed_files_hashed=1,
issue_count=1,
issues=(DepotIntegrityIssue("game/data.bin", "hash mismatch"),),
duration_ms=2,
)
with (
patch("ptlinux.cli.game_import_requires_verification", return_value=False),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli.read_current_depot_file_catalog", return_value=catalog),
patch("ptlinux.cli.preflight_managed_game", return_value=preflight),
patch("ptlinux.cli.prepare_runtime_view") as prepare,
self.assertRaisesRegex(LauncherError, "game/data.bin: hash mismatch"),
):
_play_locked(
paths,
None,
False,
False,
False,
False,
)
prepare.assert_not_called()
class CliImportVerificationTests(unittest.TestCase):
def test_pending_import_forces_full_repair_and_records_completion(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(
data=root / "data",
config=root / "config",
state=root / "state",
cache=root / "cache",
)
sync_result = DepotSyncResult(
region="Europe",
server="cdn.example",
name="PristonTale EU",
build=330,
version="330",
files_total=1,
files_planned=0,
bytes_planned=0,
files_downloaded=0,
bytes_downloaded=0,
files_deleted=0,
files_created=0,
files_quarantined=2,
quarantine_directory="/quarantine/import-1",
repair=True,
dry_run=False,
completed_at="2026-08-30T12:00:00+02:00",
)
with (
patch("ptlinux.cli._operation_guard", return_value=contextlib.nullcontext()),
patch("ptlinux.cli._load_or_default_settings", return_value=Settings()),
patch("ptlinux.cli._runner", return_value=MagicMock()),
patch("ptlinux.cli._maybe_import_game", return_value=None),
patch("ptlinux.cli.game_import_requires_verification", return_value=True),
patch("ptlinux.cli.sync_managed_game", return_value=sync_result) as sync,
patch("ptlinux.cli.mark_game_import_verified") as mark_verified,
patch("ptlinux.cli._emit"),
):
result = _native_update(
paths,
False,
False,
None,
False,
True,
)
self.assertEqual(result, 0)
sync.assert_called_once_with(
paths,
repair=True,
quarantine_unmanaged=True,
dry_run=False,
progress=None,
)
mark_verified.assert_called_once_with(
paths,
build=330,
files_quarantined=2,
quarantine_directory="/quarantine/import-1",
)
class CliRuntimeIssueTests(unittest.TestCase):
def test_discovers_a_crash_dump_created_by_the_current_launch(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
directory = Path(temporary)
crash_path = directory / "crash_Game_20260905_110706.dat"
crash_path.write_bytes(b"MDMP")
report = _current_game_crash(directory, {}, 0)
self.assertIsNotNone(report)
assert report is not None
self.assertEqual(report["path"], str(crash_path.absolute()))
self.assertEqual(report["size"], 4)
def test_prints_resource_exhaustion_before_asset_repair_advice(self) -> None:
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
_print_interrupted_game_log(
{
"path": "/tmp/game.log",
"module_code_sync_error": False,
"runtime_out_of_memory": True,
"out_of_memory_effect_paths": [
r"game\scripts\shaders\UI\Minimap.fx"
],
}
)
output = stderr.getvalue()
self.assertIn("Direct3D resource exhaustion", output)
self.assertIn("Minimap.fx", output)
self.assertIn("depot repair will not fix", output)
def test_human_failure_summary_does_not_replay_streamed_process_output(self) -> None:
outcome = CommandOutcome(
["game"],
9,
"already streamed stdout\n",
"already streamed stderr\n",
1.0,
False,
False,
)
issues = _runtime_issue_summary(outcome.stderr)
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
_print_game_outcome(outcome, None, issues)
self.assertIn("exited with status 9", stderr.getvalue())
self.assertNotIn("already streamed stderr", stderr.getvalue())
def test_crash_dump_overrides_a_zero_process_status(self) -> None:
outcome = CommandOutcome(["game"], 0, "", "", 1.0, False, False)
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
_print_game_outcome(
outcome,
None,
_runtime_issue_summary(""),
{"path": "/tmp/crash_game.dat"},
)
self.assertIn("crashed and wrote a dump", stderr.getvalue())
self.assertNotIn("exited normally", stderr.getvalue())
def test_resource_exhaustion_overrides_the_normal_exit_summary(self) -> None:
outcome = CommandOutcome(["game"], 0, "", "", 1.0, False, False)
stdout = io.StringIO()
stderr = io.StringIO()
native_log = {
"path": "/tmp/game.log",
"module_code_sync_error": False,
"runtime_out_of_memory": True,
"out_of_memory_effect_paths": [
r"game\scripts\shaders\UI\Minimap.fx"
],
}
with (
contextlib.redirect_stdout(stdout),
contextlib.redirect_stderr(stderr),
):
_print_game_outcome(
outcome,
native_log,
_runtime_issue_summary(""),
)
combined = stdout.getvalue() + stderr.getvalue()
self.assertNotIn("exited normally", combined)
self.assertIn(
"status 0 after logging Direct3D resource exhaustion",
combined,
)
self.assertIn("condition can recur", combined)
def test_default_installs_the_verified_portable_umu_helper(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
paths = AppPaths(root / "data", root / "config", root / "state", root / "cache")
installed = UmuBootstrapResult(
path=str(paths.managed_umu_executable),
version="1.4.4",
source_url="https://example.invalid/umu.tar",
archive_sha256="a" * 64,
executable_sha256="b" * 64,
changed=True,
)
with (
patch("ptlinux.cli.provision_umu", return_value=installed) as ensure,
):
result = _ensure_portable_umu_for_play(
paths,
Settings(),
dry_run=False,
)
self.assertEqual(result, installed)
ensure.assert_called_once_with(paths)
def test_auto_falls_back_to_direct_proton_if_the_umu_download_fails(self) -> None:
paths = MagicMock()
with (
patch(
"ptlinux.cli.provision_umu",
side_effect=UmuBootstrapError("network unavailable"),
),
patch(
"ptlinux.cli.select_runtime",
return_value=RuntimeSelection("proton7", "python", "/proton", "/steam"),
) as select,
contextlib.redirect_stderr(io.StringIO()),
):
result = _ensure_portable_umu_for_play(
paths,
Settings(runtime="auto"),
dry_run=False,
)
self.assertIsNone(result)
self.assertEqual(select.call_args.args[0].runtime, "proton7")
def test_auto_reports_when_neither_umu_nor_direct_proton_is_available(self) -> None:
paths = MagicMock()
with (
patch(
"ptlinux.cli.provision_umu",
side_effect=UmuBootstrapError("network unavailable"),
),
patch(
"ptlinux.cli.select_runtime",
side_effect=RuntimeErrorPT("Proton 7 missing"),
),
self.assertRaisesRegex(RuntimeError, "no usable game runtime"),
):
_ensure_portable_umu_for_play(
paths,
Settings(runtime="auto"),
dry_run=False,
)
def test_summarizes_arch_runtime_noise_without_calling_it_authentication(self) -> None:
report = _runtime_issue_summary(
"The futex facility returned an unexpected error code.\n"
"plugin.so: wrong ELF class: ELFCLASS64\n"
"winegstreamer error: Internal data stream error.\n"
"GStreamer-WARNING: Failed to load plugin 'libgstlibav.so': "
"libavfilter.so.7: cannot open shared object file: No such file or directory\n"
"GStreamer-WARNING: Failed to load plugin 'libgstsoup.so': "
"libsoup-2.4.so.1: cannot open shared object file: No such file or directory\n"
"egl: failed to create dri2 screen\n"
"vulkan: physical device queue doesn't support presenting on our surface\n"
"vulkan_make_output failed\n"
"SteamAPI_Init() failed; no appID found.\n"
"Assertion failed: module->module.SymType != SymDeferred, "
"file ../src-wine/dlls/dbghelp/module.c, line 383\n"
)
self.assertEqual(report["host_futex_fatal_count"], 1)
self.assertEqual(report["gstreamer_wrong_elf_class_count"], 1)
self.assertEqual(report["gstreamer_missing_library_count"], 2)
self.assertEqual(
report["gstreamer_missing_libraries"],
["libavfilter.so.7", "libsoup-2.4.so.1"],
)
self.assertTrue(report["gstreamer_stream_error"])
self.assertTrue(report["mesa_egl_initialization_failed"])
self.assertTrue(report["gamescope_present_queue_unsupported"])
self.assertEqual(report["gamescope_vulkan_output_failed_count"], 1)
self.assertTrue(report["steam_api_app_id_missing"])
self.assertEqual(report["dbghelp_symdeferred_assertion_count"], 1)
self.assertFalse(report["bubblewrap_uid_map_permission_denied"])
self.assertFalse(report["game_window_ready"])
def test_recognizes_a_ready_game_window_after_runtime_noise(self) -> None:
report = _runtime_issue_summary(
"plugin.so: wrong ELF class: ELFCLASS64\n"
"Game window resizing is disabled to prevent repeated Direct3D device resets.\n"
)
self.assertTrue(report["game_window_ready"])
def test_summarizes_bubblewrap_uid_map_denial(self) -> None:
report = _runtime_issue_summary(
"pressure-vessel-wrap: bwrap: setting up uid map: Permission denied\n"
)
self.assertTrue(report["bubblewrap_uid_map_permission_denied"])
def test_native_log_telemetry_counts_asset_classifications_without_paths(self) -> None:
report = game_log_telemetry_summary(
{
"failed_texture_file_count": 1,
"asset_references": {
"total_events": 1,
"unique_valid_references": 1,
"invalid_reference_events": 0,
"catalog_available": True,
"managed_integrity_failed": False,
"managed_present_paths": [],
"managed_missing_paths": [],
"publisher_unmanaged_paths": [
"game/images/UI/combobox/drop_big.png"
],
"unclassified_paths": [],
},
}
)
self.assertEqual(report["failed_texture_file_count"], 1)
self.assertEqual(
report["asset_reference_summary"],
{
"total_events": 1,
"unique_valid_references": 1,
"invalid_reference_events": 0,
"catalog_available": True,
"managed_integrity_failed": False,
"managed_present_count": 0,
"managed_missing_count": 0,
"publisher_unmanaged_count": 1,
"unclassified_count": 0,
},
)
self.assertNotIn("drop_big.png", json.dumps(report))
def test_successful_window_does_not_treat_plugin_scan_noise_as_a_runtime_fix(self) -> None:
self.assertIsNone(
recommended_runtime_comparison(
"proton7",
{
"game_window_ready": True,
"gstreamer_missing_libraries": ["libsoup-2.4.so.1"],
"gstreamer_stream_error": False,
},
)
)
def test_stream_failure_still_recommends_the_container_runtime(self) -> None:
self.assertEqual(
recommended_runtime_comparison(
"proton7",
{
"game_window_ready": True,
"gstreamer_missing_libraries": ["libsoup-2.4.so.1"],
"gstreamer_stream_error": True,
},
),
"proton7-container",
)
if __name__ == "__main__":
unittest.main()