135 lines
4.6 KiB
Python
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()
|