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

135 lines
4.6 KiB
Python

from __future__ import annotations
import contextlib
import io
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from ptlinux.window_diagnostics import (
WINDOW_DIAGNOSTIC_EVENT_LIMIT,
WINDOW_DIAGNOSTIC_PREFIX,
WINDOW_DIAGNOSTICS_ENV,
WINDOW_DIAGNOSTICS_SESSION_ENV,
WindowDiagnosticLog,
WindowDiagnosticRecorder,
parse_window_diagnostic,
)
class WindowDiagnosticTests(unittest.TestCase):
def test_local_log_keeps_bounded_samples_and_summaries_from_this_session(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "logs" / "windows-session.jsonl"
log = WindowDiagnosticLog(path, "a" * 32)
for component in ("game_window", "gamescope_host"):
payload = {
"component": component,
"session_id": "a" * 32,
"diagnostic": "focus_state_changed",
}
log.record({**payload, "session_id": "b" * 32})
for sequence in range(WINDOW_DIAGNOSTIC_EVENT_LIMIT + 10):
log.record({**payload, "sequence": sequence + 1})
for _ in range(2):
log.record({**payload, "diagnostic": "session_summary"})
payloads = [json.loads(line) for line in path.read_text().splitlines()]
self.assertEqual(path.stat().st_mode & 0o777, 0o600)
self.assertEqual({item["session_id"] for item in payloads}, {"a" * 32})
for component in ("game_window", "gamescope_host"):
records = [item for item in payloads if item["component"] == component]
self.assertEqual(len(records), WINDOW_DIAGNOSTIC_EVENT_LIMIT + 1)
self.assertEqual(records[-1]["diagnostic"], "session_summary")
def test_local_log_io_failure_warns_once_without_interrupting_play(self) -> None:
log = WindowDiagnosticLog(Path("/unused/windows-session.jsonl"), "a" * 32)
payload = {"component": "game_window", "session_id": "a" * 32, "diagnostic": "focus"}
output = io.StringIO()
with (
patch("ptlinux.window_diagnostics.Path.mkdir", side_effect=OSError("disk full")) as mkdir,
contextlib.redirect_stderr(output),
):
log.record(payload)
log.record(payload)
mkdir.assert_called_once()
self.assertEqual(output.getvalue().count("could not be saved locally"), 1)
def test_recorder_bounds_samples_and_always_emits_a_summary(self) -> None:
environment = {
WINDOW_DIAGNOSTICS_ENV: "1",
WINDOW_DIAGNOSTICS_SESSION_ENV: "a" * 32,
}
output = io.StringIO()
with contextlib.redirect_stderr(output):
recorder = WindowDiagnosticRecorder(
"gamescope_host",
environment=environment,
monotonic=lambda: 10.0,
)
for index in range(WINDOW_DIAGNOSTIC_EVENT_LIMIT + 8):
recorder.record("host_inactive_remap", attempt=index)
recorder.close(minimized_latched=True)
payloads = [
parse_window_diagnostic(line)
for line in output.getvalue().splitlines()
]
self.assertNotIn(None, payloads)
self.assertEqual(len(payloads), WINDOW_DIAGNOSTIC_EVENT_LIMIT + 1)
summary = payloads[-1]
assert summary is not None
self.assertEqual(summary["diagnostic"], "session_summary")
self.assertEqual(summary["session_id"], "a" * 32)
details = summary["details"]
assert isinstance(details, dict)
self.assertEqual(
details["counts"],
{"host_inactive_remap": WINDOW_DIAGNOSTIC_EVENT_LIMIT + 8},
)
self.assertEqual(details["sampled_events"], WINDOW_DIAGNOSTIC_EVENT_LIMIT)
self.assertEqual(details["suppressed_events"], 8)
def test_transition_records_only_state_changes(self) -> None:
output = io.StringIO()
with contextlib.redirect_stderr(output):
recorder = WindowDiagnosticRecorder(
"game_window",
environment={WINDOW_DIAGNOSTICS_ENV: "true"},
monotonic=lambda: 1.0,
)
recorder.transition("focus", (False, False), "focus_state_changed")
recorder.transition("focus", (False, False), "focus_state_changed")
recorder.transition("focus", (True, True), "focus_state_changed")
recorder.close()
payloads = [
json.loads(line.removeprefix(WINDOW_DIAGNOSTIC_PREFIX))
for line in output.getvalue().splitlines()
]
self.assertEqual(
[payload["diagnostic"] for payload in payloads],
["focus_state_changed", "focus_state_changed", "session_summary"],
)
self.assertEqual(payloads[-1]["details"]["counts"], {"focus_state_changed": 2})
def test_parser_rejects_untrusted_or_malformed_records(self) -> None:
self.assertIsNone(parse_window_diagnostic("ordinary game output\n"))
self.assertIsNone(
parse_window_diagnostic(
WINDOW_DIAGNOSTIC_PREFIX
+ '{"schema":1,"component":"other","diagnostic":"state",'
+ '"sequence":1,"elapsed_ms":0,"details":{}}\n'
)
)
self.assertIsNone(
parse_window_diagnostic(
WINDOW_DIAGNOSTIC_PREFIX + "{not-json}\n"
)
)
if __name__ == "__main__":
unittest.main()