personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_display_powersave.py
11 kB 382 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import subprocess 7from pathlib import Path 8from unittest.mock import MagicMock 9 10import pytest 11 12from solstone.think import display_powersave as dp 13from solstone.think.processing import DISPLAY_POWERSAVE_UNAVAILABLE 14 15 16@pytest.fixture(autouse=True) 17def reset_display_monitor(): 18 dp.reset_display_powersave_monitor() 19 yield 20 dp.reset_display_powersave_monitor() 21 22 23def _connector( 24 root: Path, 25 name: str, 26 *, 27 status: str | None, 28 enabled: str | None, 29 dpms: str | None, 30) -> Path: 31 path = root / name 32 path.mkdir(parents=True) 33 for attr_name, value in { 34 "status": status, 35 "enabled": enabled, 36 "dpms": dpms, 37 }.items(): 38 if value is not None: 39 (path / attr_name).write_text(value, encoding="utf-8") 40 return path 41 42 43def _read_linux(root: Path): 44 return dp.read_display_power(platform="linux", sysfs_root=root) 45 46 47def _reader(state: str, reason: str | None = None): 48 return lambda: dp.DisplayReading(state, reason) 49 50 51def test_read_linux_returns_awake_when_any_considered_connector_is_on( 52 tmp_path: Path, 53) -> None: 54 _connector( 55 tmp_path, 56 "card1-HDMI-A-2", 57 status="connected", 58 enabled="enabled", 59 dpms="On", 60 ) 61 62 assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 63 64 65@pytest.mark.parametrize("dpms", ["Off", "Standby", "Suspend"]) 66def test_read_linux_returns_asleep_when_all_considered_connectors_are_off_states( 67 tmp_path: Path, 68 dpms: str, 69) -> None: 70 _connector( 71 tmp_path, 72 "card1-HDMI-A-2", 73 status="connected", 74 enabled="enabled", 75 dpms=dpms, 76 ) 77 78 assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_ASLEEP, None) 79 80 81def test_read_linux_ignores_disconnected_and_disabled_stale_dpms( 82 tmp_path: Path, 83) -> None: 84 _connector( 85 tmp_path, 86 "card1-HDMI-A-2", 87 status="disconnected", 88 enabled="enabled", 89 dpms="On", 90 ) 91 _connector( 92 tmp_path, 93 "card1-DP-1", 94 status="connected", 95 enabled="disabled", 96 dpms="On", 97 ) 98 99 assert _read_linux(tmp_path) == dp.DisplayReading( 100 dp.DISPLAY_UNDETECTABLE, 101 dp.REASON_HEADLESS, 102 ) 103 104 105def test_read_linux_mixed_on_and_off_returns_awake(tmp_path: Path) -> None: 106 _connector( 107 tmp_path, 108 "card1-HDMI-A-2", 109 status="connected", 110 enabled="enabled", 111 dpms="Off", 112 ) 113 _connector( 114 tmp_path, 115 "card1-DP-1", 116 status="connected", 117 enabled="enabled", 118 dpms="On", 119 ) 120 121 assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 122 123 124def test_read_linux_returns_unreadable_for_missing_or_unknown_considered_dpms( 125 tmp_path: Path, 126) -> None: 127 _connector( 128 tmp_path, 129 "card1-HDMI-A-2", 130 status="connected", 131 enabled="enabled", 132 dpms="Off", 133 ) 134 _connector( 135 tmp_path, 136 "card1-DP-1", 137 status="connected", 138 enabled="enabled", 139 dpms=None, 140 ) 141 142 assert _read_linux(tmp_path) == dp.DisplayReading( 143 dp.DISPLAY_UNDETECTABLE, 144 dp.REASON_UNREADABLE, 145 ) 146 147 148def test_read_linux_on_wins_over_unreadable_sibling(tmp_path: Path) -> None: 149 _connector( 150 tmp_path, 151 "card1-HDMI-A-2", 152 status="connected", 153 enabled="enabled", 154 dpms=None, 155 ) 156 _connector( 157 tmp_path, 158 "card1-DP-1", 159 status="connected", 160 enabled="enabled", 161 dpms="On", 162 ) 163 164 assert _read_linux(tmp_path) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 165 166 167def test_read_linux_returns_headless_when_no_connected_enabled_connectors( 168 tmp_path: Path, 169) -> None: 170 _connector( 171 tmp_path, 172 "card1-HDMI-A-2", 173 status="disconnected", 174 enabled="enabled", 175 dpms="Off", 176 ) 177 178 assert _read_linux(tmp_path) == dp.DisplayReading( 179 dp.DISPLAY_UNDETECTABLE, 180 dp.REASON_HEADLESS, 181 ) 182 183 184def test_read_linux_returns_unreadable_when_no_connectors(tmp_path: Path) -> None: 185 assert _read_linux(tmp_path) == dp.DisplayReading( 186 dp.DISPLAY_UNDETECTABLE, 187 dp.REASON_UNREADABLE, 188 ) 189 190 191def test_read_linux_returns_unreadable_when_root_missing(tmp_path: Path) -> None: 192 assert _read_linux(tmp_path / "missing") == dp.DisplayReading( 193 dp.DISPLAY_UNDETECTABLE, 194 dp.REASON_UNREADABLE, 195 ) 196 197 198def test_read_linux_returns_unreadable_when_glob_raises() -> None: 199 class BrokenRoot: 200 def glob(self, _pattern): 201 raise OSError("no drm") 202 203 assert dp.read_display_power(platform="linux", sysfs_root=BrokenRoot()) == ( 204 dp.DisplayReading(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE) 205 ) 206 207 208def test_read_display_power_wraps_detector_exception_as_unreadable( 209 tmp_path: Path, 210 monkeypatch: pytest.MonkeyPatch, 211) -> None: 212 _connector( 213 tmp_path, 214 "card1-HDMI-A-2", 215 status="connected", 216 enabled="enabled", 217 dpms="On", 218 ) 219 220 def fail_read_attr(_path: Path, _name: str) -> str: 221 raise RuntimeError("boom") 222 223 monkeypatch.setattr(dp, "_read_attr", fail_read_attr) 224 225 assert _read_linux(tmp_path) == dp.DisplayReading( 226 dp.DISPLAY_UNDETECTABLE, 227 dp.REASON_UNREADABLE, 228 ) 229 230 231def test_read_display_power_dispatches_linux_and_darwin( 232 tmp_path: Path, 233 monkeypatch: pytest.MonkeyPatch, 234) -> None: 235 monkeypatch.setattr( 236 dp, 237 "_read_linux", 238 lambda root: dp.DisplayReading(dp.DISPLAY_AWAKE, None), 239 ) 240 monkeypatch.setattr( 241 dp, 242 "_read_macos", 243 lambda: dp.DisplayReading(dp.DISPLAY_ASLEEP, None), 244 ) 245 246 assert dp.read_display_power(platform="linux", sysfs_root=tmp_path).state == ( 247 dp.DISPLAY_AWAKE 248 ) 249 assert dp.read_display_power(platform="darwin", sysfs_root=tmp_path).state == ( 250 dp.DISPLAY_ASLEEP 251 ) 252 253 254def test_read_display_power_returns_unsupported_platform(tmp_path: Path) -> None: 255 assert dp.read_display_power(platform="win32", sysfs_root=tmp_path) == ( 256 dp.DisplayReading(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNSUPPORTED_PLATFORM) 257 ) 258 259 260def test_parse_macos_log_uses_last_display_marker() -> None: 261 text = "\n".join( 262 [ 263 "2026-01-01 Display is turned off", 264 "2026-01-01 Display is turned on", 265 ] 266 ) 267 268 assert dp._parse_macos_log(text) == dp.DisplayReading(dp.DISPLAY_AWAKE, None) 269 270 271def test_parse_macos_log_without_marker_returns_no_window_server() -> None: 272 assert dp._parse_macos_log("unrelated line") == dp.DisplayReading( 273 dp.DISPLAY_UNDETECTABLE, 274 dp.REASON_NO_WINDOW_SERVER, 275 ) 276 277 278@pytest.mark.parametrize( 279 ("asleep", "expected_state"), 280 [(True, dp.DISPLAY_ASLEEP), (False, dp.DISPLAY_AWAKE)], 281) 282def test_read_macos_uses_coregraphics_sleep_state_when_main_display_exists( 283 monkeypatch: pytest.MonkeyPatch, 284 asleep: bool, 285 expected_state: str, 286) -> None: 287 monkeypatch.setattr(dp, "_macos_main_display_id", lambda: 1) 288 monkeypatch.setattr(dp, "_macos_display_is_asleep", lambda display_id: asleep) 289 290 assert dp._read_macos() == dp.DisplayReading(expected_state, None) 291 292 293def test_read_macos_falls_back_to_pmset_log_when_main_display_is_zero( 294 monkeypatch: pytest.MonkeyPatch, 295) -> None: 296 monkeypatch.setattr(dp, "_macos_main_display_id", lambda: 0) 297 monkeypatch.setattr(dp, "_macos_pmset_log", lambda: "Display is turned off") 298 299 assert dp._read_macos() == dp.DisplayReading(dp.DISPLAY_ASLEEP, None) 300 301 302def test_macos_pmset_log_contains_timeout_and_oserror( 303 monkeypatch: pytest.MonkeyPatch, 304) -> None: 305 def timeout(*_args, **_kwargs): 306 raise subprocess.TimeoutExpired(["pmset", "-g", "log"], timeout=1) 307 308 monkeypatch.setattr(dp.subprocess, "run", timeout) 309 assert dp._macos_pmset_log() == "" 310 311 def oserror(*_args, **_kwargs): 312 raise OSError("pmset missing") 313 314 monkeypatch.setattr(dp.subprocess, "run", oserror) 315 assert dp._macos_pmset_log() == "" 316 317 318def test_monitor_debounces_only_after_threshold_from_injected_now() -> None: 319 monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 320 321 first = monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 322 before_threshold = monitor.poll(now=119.0, read=_reader(dp.DISPLAY_ASLEEP)) 323 after_threshold = monitor.poll(now=240.0, read=_reader(dp.DISPLAY_ASLEEP)) 324 325 assert first.debounced is False 326 assert before_threshold.debounced is False 327 assert after_threshold.debounced is True 328 329 330def test_monitor_wake_closes_immediately_and_resets_since() -> None: 331 monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 332 monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 333 monitor.poll(now=240.0, read=_reader(dp.DISPLAY_ASLEEP)) 334 335 awake = monitor.poll(now=241.0, read=_reader(dp.DISPLAY_AWAKE)) 336 next_asleep = monitor.poll(now=242.0, read=_reader(dp.DISPLAY_ASLEEP)) 337 338 assert awake.available is True 339 assert awake.asleep is False 340 assert awake.debounced is False 341 assert next_asleep.debounced is False 342 343 344def test_monitor_undetectable_resets_snapshot_and_debounce() -> None: 345 monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 346 monitor.poll(now=0.0, read=_reader(dp.DISPLAY_ASLEEP)) 347 undetectable = monitor.poll( 348 now=60.0, 349 read=_reader(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE), 350 ) 351 next_asleep = monitor.poll(now=180.0, read=_reader(dp.DISPLAY_ASLEEP)) 352 353 assert undetectable == DISPLAY_POWERSAVE_UNAVAILABLE 354 assert next_asleep.debounced is False 355 356 357def test_monitor_capability_known_is_sticky_until_reset() -> None: 358 monitor = dp.DisplayPowersaveMonitor(debounce_s=120.0) 359 360 assert monitor.capability_known() is False 361 monitor.poll(now=0.0, read=_reader(dp.DISPLAY_AWAKE)) 362 assert monitor.capability_known() is True 363 monitor.poll( 364 now=1.0, 365 read=_reader(dp.DISPLAY_UNDETECTABLE, dp.REASON_UNREADABLE), 366 ) 367 assert monitor.capability_known() is True 368 monitor.reset() 369 assert monitor.capability_known() is False 370 371 372def test_singleton_last_and_reset_are_non_polling( 373 monkeypatch: pytest.MonkeyPatch, 374) -> None: 375 snapshot = dp._MONITOR.poll(now=0.0, read=_reader(dp.DISPLAY_AWAKE)) 376 read = MagicMock(side_effect=AssertionError("last should not poll")) 377 monkeypatch.setattr(dp, "read_display_power", read) 378 379 assert dp.last_display_powersave() == snapshot 380 read.assert_not_called() 381 dp.reset_display_powersave_monitor() 382 assert dp.last_display_powersave() == DISPLAY_POWERSAVE_UNAVAILABLE