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()