Release 1.1.3 with game window and Gamescope lifecycle fixes
This commit is contained in:
134
tests/test_ptlinux_window_diagnostics.py
Normal file
134
tests/test_ptlinux_window_diagnostics.py
Normal file
@@ -0,0 +1,134 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user