zapret-kvn/tests/test_app_window_presence.py
loop-uh 5965af36c5
Some checks failed
Windows project source guards / test (push) Failing after 26s
Возврат окна из трея: хватка памяти растёт вместе с программой, страницы строятся заранее; в журнал — причины для зеркал
Сегодняшние правки Zapret 2 GUI по журналу пользователя перенесены в KVN. 1) Пока окно скрыто, раз в минуту проверяется рабочий набор, и при росте от 16 МБ хватка и снимок страниц берутся заново (запуск в трей брал их на пятой секунде, пока программа ещё не закончила стартовые работы). 2) Страницы окна (DeferredPage) строились только при первом открытии, то есть под щелчком; ui/page_warmup.py собирает страницы узлов, подписок, настроек, обновлений и журнала заранее, но только пока окно скрыто или свёрнуто, через 2 минуты после запуска и когда человек минуту не трогал мышь и клавиатуру: по одной с паузой 3 с (решение пользователя 2026-10-20 для GUI, цена — память страниц в трее). 3) Общее ядро обновления (копия из Zapret 2 GUI) пишет в журнал, откуда получен список имён git-сервера и почему не открылось ни одно имя.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 05:55:59 -04:00

482 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Присутствие окна: память подтягивается при возврате из долгого фона (xray_fluent.ui.window_presence).
Жалоба пользователя 2026-10-17: окно долго в фоне, потом открываешь из панели
задач — «ничего не работает»: Windows отобрала память программы, и окно тянуло
её с диска по странице. Проверяется порядок действий, а не скорость диска.
Keep the ``test_app_*`` prefix (QApplication before tests/test_engine_process_stop.py).
"""
from __future__ import annotations
import inspect
import os
import re
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QWidget
_existing = QApplication.instance()
if _existing is not None and not isinstance(_existing, QApplication):
raise RuntimeError("widget tests need a QApplication")
app = _existing or QApplication([])
from xray_fluent.platform.windows import working_set
from xray_fluent.platform.windows.working_set import PrefetchResult, WorkingSetHold, WorkingSetSnapshot
from xray_fluent.ui import window_presence
from xray_fluent.ui.window_presence import WindowPresence, is_native_restore_command
MAIN_WINDOW = Path(__file__).resolve().parent.parent / "xray_fluent" / "ui" / "main_window.py"
SNAPSHOT = WorkingSetSnapshot(ranges=((0x1000, 0x2000), (0x8000, 0x1000)), total_bytes=0x3000, taken_ms=1.0)
class _Clock:
def __init__(self) -> None:
self.now = 1000.0
def __call__(self) -> float:
return self.now
def _make(window):
calls: list[str] = []
clock = _Clock()
presence = WindowPresence(
window,
settle_ms=60_000,
report_ms=60_000,
min_away_for_prefetch_s=20.0,
take_snapshot=lambda: calls.append("snapshot") or SNAPSHOT,
prefetch=lambda snap: calls.append("prefetch") or PrefetchResult(len(snap.ranges), snap.total_bytes, 1.0, True),
start_worker=lambda fn: fn(),
clock=clock,
)
return presence, calls, clock
class WindowPresenceTests(unittest.TestCase):
def _window(self, *, shown: bool = True) -> QWidget:
window = QWidget()
self.addCleanup(window.deleteLater)
if shown:
window.show()
app.processEvents()
return window
def test_hidden_window_starts_away(self) -> None:
presence, _calls, _clock = _make(self._window(shown=False))
self.assertTrue(presence.away)
def test_pages_are_remembered_once_the_window_stayed_away(self) -> None:
window = self._window()
presence, calls, _clock = _make(window)
window.hide()
self.assertTrue(presence._settle_timer.isActive())
presence._settle_timer.stop() # так таймер выглядит после срабатывания
presence._on_settle_timeout()
self.assertEqual(calls, ["snapshot"])
def test_showing_the_window_cancels_a_pending_snapshot(self) -> None:
window = self._window()
presence, calls, _clock = _make(window)
window.hide()
window.show()
app.processEvents()
self.assertFalse(presence._settle_timer.isActive())
presence._on_settle_timeout()
self.assertEqual(calls, [])
def test_return_intent_comes_before_qt_shows_a_minimized_window(self) -> None:
window = self._window()
presence, calls, clock = _make(window)
window.setWindowState(Qt.WindowState.WindowMinimized)
app.processEvents()
presence._on_settle_timeout()
clock.now += 600
calls.clear()
presence.return_intent()
self.assertEqual(calls, ["prefetch"])
# Позднее событие Qt уже ничего не повторяет.
window.setWindowState(Qt.WindowState.WindowNoState)
app.processEvents()
self.assertEqual(calls, ["prefetch"])
def test_short_absence_does_not_prefetch(self) -> None:
window = self._window()
presence, calls, clock = _make(window)
window.hide()
presence._on_settle_timeout()
calls.clear()
clock.now += 3
presence.return_intent()
self.assertEqual(calls, [])
def test_return_before_the_snapshot_is_ready_does_not_fail(self) -> None:
window = self._window()
presence, calls, clock = _make(window)
window.hide()
clock.now += 600
presence.return_intent()
self.assertEqual(calls, [])
def test_return_report_goes_to_the_log(self) -> None:
window = self._window()
presence, _calls, clock = _make(window)
window.hide()
presence._on_settle_timeout()
clock.now += 600
presence.return_intent()
with patch.object(window_presence.LOG, "info") as info:
presence._report_return()
text = info.call_args.args[0] % info.call_args.args[1:]
self.assertIn("window returned from background (600 s)", text)
self.assertIn("prefetched", text)
class NativeRestoreTests(unittest.TestCase):
def test_restore_command_is_recognised_and_other_commands_are_not(self) -> None:
def message(command: int):
return SimpleNamespace(message=window_presence.WM_SYSCOMMAND, wParam=command)
with (
patch.object(window_presence.sys, "platform", "win32"),
patch("ctypes.wintypes.MSG") as msg_type,
):
msg_type.from_address.side_effect = lambda address: message(address)
self.assertTrue(is_native_restore_command(window_presence.SC_RESTORE))
self.assertTrue(is_native_restore_command(window_presence.SC_RESTORE | 0x2))
self.assertFalse(is_native_restore_command(0xF020)) # SC_MINIMIZE
def test_other_systems_never_see_a_restore_command(self) -> None:
with patch.object(window_presence.sys, "platform", "linux"):
self.assertFalse(is_native_restore_command(1))
def test_main_window_tells_presence_before_qt_handles_the_command(self) -> None:
source = MAIN_WINDOW.read_text(encoding="utf-8")
match = re.search(r"def nativeEvent\(.*?\n(?= def )", source, re.S)
self.assertIsNotNone(match)
body = match.group(0)
self.assertLess(body.index("is_native_restore_command"), body.index("super().nativeEvent"))
self.assertIn("_window_presence.return_intent()", body)
class TrayToggleTests(unittest.TestCase):
def _toggle_source(self) -> str:
source = MAIN_WINDOW.read_text(encoding="utf-8")
return re.search(r"def _toggle_window_visible\(.*?\n(?= def )", source, re.S).group(0)
def test_minimized_window_is_brought_back_not_hidden(self) -> None:
# У свёрнутого окна isVisible() тоже True: клик по значку в трее прятал его.
body = self._toggle_source()
self.assertIn("self.isVisible() and not self.isMinimized()", body)
self.assertIn("showNormal()", body)
def test_tray_show_tells_presence_first(self) -> None:
body = self._toggle_source()
self.assertLess(body.index("return_intent()"), body.index("self.show()"))
class WorkingSetTests(unittest.TestCase):
def test_neighbouring_pages_become_one_range(self) -> None:
page = working_set.PAGE_SIZE
merged = working_set._merge_pages([0x10000, 0x10000 + page, 0x10000 + 2 * page, 0x40000])
self.assertEqual(merged, [(0x10000, 3 * page), (0x40000, page)])
def test_other_systems_do_nothing(self) -> None:
with patch.object(working_set.sys, "platform", "linux"):
self.assertIsNone(working_set.snapshot_working_set())
self.assertIsNone(working_set.prefetch_working_set(SNAPSHOT))
def test_the_program_never_empties_its_own_working_set(self) -> None:
for module in (working_set, window_presence):
source = inspect.getsource(module)
self.assertNotIn("EmptyWorkingSet(", source)
# «Минус единицы» в SetProcessWorkingSetSizeEx — это выгрузка памяти.
self.assertNotIn("(-1", source)
self.assertNotIn("c_size_t(-1)", source)
# Работает с минимумом рабочего набора только модуль working_set, не само окно.
self.assertNotIn("SetProcessWorkingSetSize", inspect.getsource(window_presence))
self.assertNotIn("import psutil", inspect.getsource(window_presence))
def test_weak_pc_with_under_a_gigabyte_free_is_still_protected(self) -> None:
mb = 1 << 20
self.assertIsNotNone(working_set.hold_size_for(80 * mb, 700 * mb))
size = 80 * mb + working_set.HOLD_MARGIN_BYTES
self.assertIsNone(working_set.hold_size_for(80 * mb, size + working_set.HOLD_SPARE_BYTES - 1))
self.assertIsNone(working_set.hold_size_for(None, None))
self.assertIsNone(working_set.hold_size_for(working_set.HOLD_MIN_BYTES - 1, None))
def test_hold_and_counters_do_nothing_outside_windows(self) -> None:
with patch.object(working_set.sys, "platform", "linux"):
self.assertIsNone(working_set.hold_working_set(100 << 20))
self.assertFalse(working_set.release_working_set_hold())
self.assertIsNone(working_set.current_working_set_bytes())
self.assertIsNone(working_set.available_physical_bytes())
self.assertIsNone(working_set.process_page_faults())
class EarlyAwayIntentTests(unittest.TestCase):
"""Windows сжимает набор свёрнутого окна сразу: защита ставится до этого, а не после."""
def _window(self) -> QWidget:
window = QWidget()
self.addCleanup(window.deleteLater)
window.show()
app.processEvents()
return window
def _make(self, window, *, snapshots=None):
events: list[str] = []
queue = list(snapshots or [SNAPSHOT])
presence = WindowPresence(
window,
settle_ms=60_000,
report_ms=60_000,
min_away_for_prefetch_s=20.0,
take_snapshot=lambda: events.append("snapshot") or (queue.pop(0) if len(queue) > 1 else queue[0]),
prefetch=lambda snap: events.append("prefetch") or PrefetchResult(len(snap.ranges), snap.total_bytes, 1.0, True),
hold=lambda: events.append("hold") or WorkingSetHold(bytes=80 << 20, ok=True, error=0),
release_hold=lambda: events.append("release"),
start_worker=lambda fn: fn(),
clock=_Clock(),
)
events.clear()
return presence, events
def test_intent_holds_and_remembers_pages_before_the_window_leaves(self) -> None:
window = self._window()
presence, events = self._make(window)
presence.away_intent()
self.assertEqual(events, ["hold", "snapshot"])
# Состояние «в фоне» сам сигнал не ставит: сворачивание могло не состояться.
self.assertFalse(presence.away)
def test_the_late_qt_event_does_not_repeat_the_snapshot_but_keeps_the_hold(self) -> None:
window = self._window()
presence, events = self._make(window)
presence.away_intent()
events.clear()
window.hide()
app.processEvents()
self.assertTrue(presence.away)
self.assertEqual(events, ["hold"])
def test_window_that_stayed_on_screen_gets_its_protection_removed(self) -> None:
window = self._window()
presence, events = self._make(window)
presence.away_intent()
events.clear()
presence._on_intent_timeout()
self.assertEqual(events, ["release"])
def test_window_that_left_is_not_released_by_the_intent_timer(self) -> None:
window = self._window()
presence, events = self._make(window)
presence.away_intent()
window.hide()
app.processEvents()
events.clear()
presence._on_intent_timeout()
self.assertEqual(events, [])
def test_return_releases_the_hold_even_after_a_short_absence(self) -> None:
window = self._window()
presence, events = self._make(window)
window.hide()
app.processEvents()
events.clear()
presence.return_intent()
self.assertEqual(events, ["release"])
def test_poorer_snapshot_taken_after_the_trim_does_not_replace_the_full_one(self) -> None:
full = WorkingSetSnapshot(ranges=((0x1000, 0x9000),), total_bytes=0x9000, taken_ms=1.0)
trimmed = WorkingSetSnapshot(ranges=((0x1000, 0x1000),), total_bytes=0x1000, taken_ms=1.0)
window = self._window()
presence, _events = self._make(window, snapshots=[full, trimmed])
presence.away_intent()
window.hide()
app.processEvents()
presence._on_settle_timeout()
self.assertIs(presence._snapshot, full)
def test_hold_size_follows_the_hot_size_not_the_trimmed_one(self) -> None:
window = self._window()
presence = WindowPresence(window, start_worker=lambda fn: fn())
presence._hot_bytes = 150 << 20
with (
patch.object(window_presence, "current_working_set_bytes", return_value=10 << 20),
patch.object(window_presence, "available_physical_bytes", return_value=4096 << 20),
patch.object(window_presence, "hold_working_set") as hold,
):
presence._hold_hot_set()
self.assertGreaterEqual(hold.call_args.args[0], 150 << 20)
def test_minimize_command_is_recognised_and_main_window_tells_presence_first(self) -> None:
def message(command: int):
return SimpleNamespace(message=window_presence.WM_SYSCOMMAND, wParam=command)
with (
patch.object(window_presence.sys, "platform", "win32"),
patch("ctypes.wintypes.MSG") as msg_type,
):
msg_type.from_address.side_effect = lambda address: message(address)
self.assertTrue(window_presence.is_native_minimize_command(window_presence.SC_MINIMIZE))
self.assertFalse(window_presence.is_native_minimize_command(window_presence.SC_RESTORE))
source = MAIN_WINDOW.read_text(encoding="utf-8")
body = re.search(r"def nativeEvent\(.*?\n(?= def )", source, re.S).group(0)
self.assertIn("is_native_minimize_command", body)
self.assertIn("_window_presence.away_intent()", body)
for name in ("_toggle_window_visible", "closeEvent"):
method = re.search(rf"def {name}\(.*?(?=\n def |\Z)", source, re.S).group(0)
self.assertLess(method.index("away_intent()"), method.index("self.hide()"), name)
class AwayHoldRefreshTests(unittest.TestCase):
"""Запуск в трей: хватка берётся рано, потом память растёт — хватка растёт вместе с ней."""
def _make(self, window):
sizes: list[int] = []
def hold():
size = (len(sizes) + 1) * 100 << 20
sizes.append(size)
return WorkingSetHold(bytes=size, ok=True, error=0)
window.show()
app.processEvents()
presence = WindowPresence(
window,
settle_ms=60_000,
report_ms=60_000,
min_away_for_prefetch_s=20.0,
take_snapshot=lambda: SNAPSHOT,
prefetch=lambda snap: PrefetchResult(len(snap.ranges), snap.total_bytes, 1.0, True),
hold=hold,
release_hold=lambda: None,
start_worker=lambda fn: fn(),
clock=_Clock(),
)
sizes.clear()
window.hide()
app.processEvents()
return presence, sizes
def _window(self) -> QWidget:
window = QWidget()
self.addCleanup(window.deleteLater)
return window
def test_hold_is_raised_when_the_program_grew_while_hidden(self) -> None:
presence, sizes = self._make(self._window())
self.assertEqual(sizes, [100 << 20])
with patch.object(window_presence, "current_working_set_bytes", return_value=150 << 20):
presence._refresh_hold_if_grown()
self.assertEqual(sizes, [100 << 20, 200 << 20])
def test_hold_is_not_touched_when_nothing_grew(self) -> None:
presence, sizes = self._make(self._window())
with patch.object(window_presence, "current_working_set_bytes", return_value=105 << 20):
presence._refresh_hold_if_grown()
self.assertEqual(sizes, [100 << 20])
def test_refresh_runs_only_while_the_window_is_away(self) -> None:
window = self._window()
presence, sizes = self._make(window)
self.assertTrue(presence._away_refresh_timer.isActive())
window.show()
app.processEvents()
self.assertFalse(presence._away_refresh_timer.isActive())
sizes.clear()
with patch.object(window_presence, "current_working_set_bytes", return_value=900 << 20):
presence._on_away_refresh()
self.assertEqual(sizes, [])
class DashboardVisibilityTests(unittest.TestCase):
"""Скрытое окно не обновляет дашборд: изменения раскладки копились бы до показа."""
def _window(self, *, on_dashboard=True, visible=True, minimized=False):
from xray_fluent.ui.main_window import MainWindow
dashboard = object()
window = SimpleNamespace(
dashboard_page=dashboard,
stackedWidget=SimpleNamespace(currentWidget=lambda: dashboard if on_dashboard else object()),
isVisible=lambda: visible,
isMinimized=lambda: minimized,
)
window._is_dashboard_active = lambda: MainWindow._is_dashboard_active(window)
return MainWindow, window
def test_dashboard_is_active_only_when_the_window_is_on_screen(self) -> None:
cls, window = self._window()
self.assertTrue(cls._is_dashboard_active(window))
for kwargs in ({"visible": False}, {"minimized": True}, {"on_dashboard": False}):
_cls, window = self._window(**kwargs)
self.assertFalse(cls._is_dashboard_active(window), kwargs)
def test_metrics_wait_while_hidden_and_arrive_after_the_window_returns(self) -> None:
from unittest.mock import Mock
cls, window = self._window(visible=False)
window._deferred_dashboard_metrics = None
window._deferred_process_stats = None
window._has_deferred_process_stats = False
window.dashboard_page = Mock()
window.stackedWidget = SimpleNamespace(currentWidget=lambda: window.dashboard_page)
window._is_dashboard_active = lambda: cls._is_dashboard_active(window)
cls._on_live_metrics_updated(window, {"down_bps": 5.0, "up_bps": 2.0, "latency_ms": 40, "process_stats": [1]})
window.dashboard_page.set_live_metrics.assert_not_called()
window.isVisible = lambda: True
cls._flush_deferred_dashboard_metrics(window)
window.dashboard_page.set_live_metrics.assert_called_once_with(5.0, 2.0, 40)
window.dashboard_page.set_process_stats.assert_called_once_with([1])
self.assertIsNone(window._deferred_dashboard_metrics)
if __name__ == "__main__":
unittest.main()