All checks were successful
Zapret source guards / validate (push) Successful in 4s
logs и tmp переезжают в user\ (пользовательское состояние, переживает обновления), themes, ico и справка help — в system\ (поставка, заменяется установщиком целиком). Иконки исходников перенесены в src/system/ico зеркально раскладке установки. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3706 lines
154 KiB
Python
3706 lines
154 KiB
Python
from __future__ import annotations
|
||
|
||
import os
|
||
import inspect
|
||
import sys
|
||
import tempfile
|
||
import types
|
||
import unittest
|
||
import weakref
|
||
from ctypes import addressof
|
||
from ctypes import wintypes
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
from unittest.mock import Mock, call, patch
|
||
|
||
|
||
PROJECT_SRC = Path(__file__).resolve().parents[1] / "src"
|
||
if str(PROJECT_SRC) not in sys.path:
|
||
sys.path.insert(0, str(PROJECT_SRC))
|
||
|
||
|
||
class StartupRuntimeSetupTests(unittest.TestCase):
|
||
def test_post_interactive_startup_steps_have_no_fixed_gap(self) -> None:
|
||
from main import startup_coordinator
|
||
|
||
self.assertEqual(startup_coordinator.STARTUP_STEP_GAP_MS, 0)
|
||
|
||
def test_phase_two_completion_dispatches_deferred_autostart(self) -> None:
|
||
from main import startup_coordinator
|
||
from main.startup_coordinator import StartupCoordinator
|
||
|
||
class Runtime:
|
||
def __init__(self) -> None:
|
||
self.autostart_calls: list[str | None] = []
|
||
self.calls: list[str] = []
|
||
|
||
def init_launch_runtime_api(self) -> None:
|
||
self.calls.append("runtime_api")
|
||
|
||
def init_launch_runtime(self) -> None:
|
||
self.calls.append("runtime")
|
||
|
||
def init_process_monitor(self) -> None:
|
||
self.calls.append("process_monitor")
|
||
|
||
def init_core_startup(self) -> None:
|
||
self.calls.append("core_startup")
|
||
|
||
def snapshot(self):
|
||
return SimpleNamespace(launch_method="zapret2_mode")
|
||
|
||
def start_autostart(self, launch_method: str | None = None) -> None:
|
||
self.calls.append("autostart")
|
||
self.autostart_calls.append(launch_method)
|
||
|
||
runtime = Runtime()
|
||
window_shell = SimpleNamespace(
|
||
start_in_tray=False,
|
||
set_status=Mock(),
|
||
mark_startup_interactive=Mock(),
|
||
mark_startup_core_ready=Mock(),
|
||
mark_startup_post_init_done=Mock(),
|
||
init_theme_manager=Mock(side_effect=lambda: runtime.calls.append("theme")),
|
||
)
|
||
tray = SimpleNamespace(
|
||
init=Mock(),
|
||
is_initialized=Mock(return_value=False),
|
||
)
|
||
coordinator = StartupCoordinator(
|
||
runtime_feature=runtime,
|
||
tray_feature=tray,
|
||
window_shell=window_shell,
|
||
log_startup_metric=Mock(),
|
||
migrate_gui_autostart=Mock(return_value=False),
|
||
)
|
||
scheduled: list[tuple[int, object]] = []
|
||
timer_delays: list[int] = []
|
||
background_targets: list[object] = []
|
||
|
||
with (
|
||
patch.object(startup_coordinator, "run_queued", side_effect=lambda callback: scheduled.append((0, callback))),
|
||
patch.object(
|
||
startup_coordinator.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay_ms, callback: (
|
||
timer_delays.append(int(delay_ms)),
|
||
scheduled.append((int(delay_ms), callback)),
|
||
),
|
||
),
|
||
patch.object(
|
||
startup_coordinator,
|
||
"start_daemon_thread",
|
||
side_effect=lambda name, target: background_targets.append((name, target)),
|
||
),
|
||
):
|
||
coordinator.run_async_init()
|
||
self.assertEqual(runtime.calls, [])
|
||
while scheduled:
|
||
_delay_ms, callback = scheduled.pop(0)
|
||
callback()
|
||
step_targets = [t for name, t in background_targets if name.startswith("StartupStep-")]
|
||
self.assertEqual(len(step_targets), 1)
|
||
self.assertIn("GuiAutostartMigration", [name for name, _t in background_targets])
|
||
step_targets[0]()
|
||
while scheduled:
|
||
_delay_ms, callback = scheduled.pop(0)
|
||
callback()
|
||
|
||
self.assertTrue(coordinator._post_init_scheduled)
|
||
self.assertEqual(runtime.autostart_calls, ["zapret2_mode"])
|
||
metric_names = [call.args[0] for call in coordinator.log_startup_metric.call_args_list]
|
||
for metric_name in (
|
||
"StartupStepLaunchRuntimeApi",
|
||
"StartupStepLaunchRuntime",
|
||
"StartupStepProcessMonitor",
|
||
"StartupStepCoreStartup",
|
||
"StartupStepStartupCoreReady",
|
||
):
|
||
self.assertIn(metric_name, metric_names)
|
||
window_shell.mark_startup_interactive.assert_not_called()
|
||
window_shell.mark_startup_post_init_done.assert_called_once()
|
||
self.assertIn(startup_coordinator.STARTUP_STEP_GAP_MS, timer_delays)
|
||
self.assertIn(startup_coordinator.STARTUP_DPI_AUTOSTART_DELAY_MS, timer_delays)
|
||
self.assertEqual(
|
||
runtime.calls,
|
||
[
|
||
"runtime_api",
|
||
"runtime",
|
||
"autostart",
|
||
"theme",
|
||
"process_monitor",
|
||
"core_startup",
|
||
],
|
||
)
|
||
|
||
def test_visual_and_background_startup_steps_run_after_deferred_autostart(self) -> None:
|
||
from main import startup_coordinator
|
||
from main.startup_coordinator import StartupCoordinator
|
||
|
||
class Runtime:
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
|
||
def init_launch_runtime_api(self) -> None:
|
||
self.calls.append("runtime_api")
|
||
|
||
def init_launch_runtime(self) -> None:
|
||
self.calls.append("runtime")
|
||
|
||
def init_process_monitor(self) -> None:
|
||
self.calls.append("process_monitor")
|
||
|
||
def init_core_startup(self) -> None:
|
||
self.calls.append("core_startup")
|
||
|
||
def snapshot(self):
|
||
return SimpleNamespace(launch_method="zapret2_mode")
|
||
|
||
def start_autostart(self, launch_method: str | None = None) -> None:
|
||
self.calls.append(f"autostart:{launch_method}")
|
||
|
||
runtime = Runtime()
|
||
window_shell = SimpleNamespace(
|
||
start_in_tray=False,
|
||
set_status=Mock(),
|
||
mark_startup_interactive=Mock(),
|
||
mark_startup_core_ready=Mock(),
|
||
mark_startup_post_init_done=Mock(),
|
||
init_theme_manager=Mock(side_effect=lambda: runtime.calls.append("theme")),
|
||
)
|
||
tray = SimpleNamespace(
|
||
init=Mock(),
|
||
is_initialized=Mock(return_value=False),
|
||
)
|
||
coordinator = StartupCoordinator(
|
||
runtime_feature=runtime,
|
||
tray_feature=tray,
|
||
window_shell=window_shell,
|
||
log_startup_metric=Mock(),
|
||
migrate_gui_autostart=Mock(return_value=False),
|
||
)
|
||
scheduled: list[tuple[int, object]] = []
|
||
background_targets: list[object] = []
|
||
|
||
with (
|
||
patch.object(startup_coordinator, "run_queued", side_effect=lambda callback: scheduled.append((0, callback))),
|
||
patch.object(
|
||
startup_coordinator.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay_ms, callback: scheduled.append((int(delay_ms), callback)),
|
||
),
|
||
patch.object(
|
||
startup_coordinator,
|
||
"start_daemon_thread",
|
||
side_effect=lambda name, target: background_targets.append((name, target)),
|
||
create=True,
|
||
),
|
||
):
|
||
coordinator.run_async_init()
|
||
while scheduled:
|
||
_delay_ms, callback = scheduled.pop(0)
|
||
callback()
|
||
|
||
self.assertEqual(
|
||
runtime.calls,
|
||
["runtime_api", "runtime", "autostart:zapret2_mode", "theme", "process_monitor"],
|
||
)
|
||
step_targets = [t for name, t in background_targets if name.startswith("StartupStep-")]
|
||
self.assertEqual(len(step_targets), 1)
|
||
window_shell.mark_startup_post_init_done.assert_called_once()
|
||
|
||
step_targets[0]()
|
||
while scheduled:
|
||
_delay_ms, callback = scheduled.pop(0)
|
||
callback()
|
||
|
||
self.assertEqual(
|
||
runtime.calls,
|
||
[
|
||
"runtime_api",
|
||
"runtime",
|
||
"autostart:zapret2_mode",
|
||
"theme",
|
||
"process_monitor",
|
||
"core_startup",
|
||
],
|
||
)
|
||
window_shell.mark_startup_post_init_done.assert_called_once()
|
||
|
||
def test_deferred_autostart_does_not_wait_for_core_startup_file_check(self) -> None:
|
||
from main import startup_coordinator
|
||
from main.startup_coordinator import StartupCoordinator
|
||
|
||
class Runtime:
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
|
||
def init_launch_runtime_api(self) -> None:
|
||
self.calls.append("runtime_api")
|
||
|
||
def init_launch_runtime(self) -> None:
|
||
self.calls.append("runtime")
|
||
|
||
def init_process_monitor(self) -> None:
|
||
self.calls.append("process_monitor")
|
||
|
||
def init_core_startup(self) -> None:
|
||
self.calls.append("core_startup")
|
||
|
||
def snapshot(self):
|
||
return SimpleNamespace(launch_method="zapret2_mode")
|
||
|
||
def start_autostart(self, launch_method: str | None = None) -> None:
|
||
self.calls.append(f"autostart:{launch_method}")
|
||
|
||
runtime = Runtime()
|
||
window_shell = SimpleNamespace(
|
||
start_in_tray=False,
|
||
set_status=Mock(),
|
||
mark_startup_interactive=Mock(),
|
||
mark_startup_core_ready=Mock(),
|
||
mark_startup_post_init_done=Mock(),
|
||
init_theme_manager=Mock(side_effect=lambda: runtime.calls.append("theme")),
|
||
)
|
||
tray = SimpleNamespace(init=Mock(), is_initialized=Mock(return_value=False))
|
||
coordinator = StartupCoordinator(
|
||
runtime_feature=runtime,
|
||
tray_feature=tray,
|
||
window_shell=window_shell,
|
||
log_startup_metric=Mock(),
|
||
migrate_gui_autostart=Mock(return_value=False),
|
||
)
|
||
scheduled: list[tuple[int, object]] = []
|
||
background_targets: list[object] = []
|
||
|
||
with (
|
||
patch.object(startup_coordinator, "run_queued", side_effect=lambda callback: scheduled.append((0, callback))),
|
||
patch.object(
|
||
startup_coordinator.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay_ms, callback: scheduled.append((int(delay_ms), callback)),
|
||
),
|
||
patch.object(
|
||
startup_coordinator,
|
||
"start_daemon_thread",
|
||
side_effect=lambda name, target: background_targets.append((name, target)),
|
||
create=True,
|
||
),
|
||
):
|
||
coordinator.run_async_init()
|
||
while scheduled:
|
||
_delay_ms, callback = scheduled.pop(0)
|
||
callback()
|
||
|
||
self.assertEqual(
|
||
runtime.calls,
|
||
[
|
||
"runtime_api",
|
||
"runtime",
|
||
"autostart:zapret2_mode",
|
||
"theme",
|
||
"process_monitor",
|
||
],
|
||
)
|
||
step_targets = [t for name, t in background_targets if name.startswith("StartupStep-")]
|
||
self.assertEqual(len(step_targets), 1)
|
||
window_shell.mark_startup_post_init_done.assert_called_once()
|
||
|
||
step_targets[0]()
|
||
|
||
self.assertEqual(runtime.calls[-1], "core_startup")
|
||
|
||
def test_page_actions_are_built_after_notifications_are_attached(self) -> None:
|
||
from main import window_runtime_setup
|
||
|
||
calls: list[str] = []
|
||
window = SimpleNamespace()
|
||
app_runtime = SimpleNamespace(
|
||
features=SimpleNamespace(),
|
||
state=SimpleNamespace(),
|
||
)
|
||
page_actions = object()
|
||
|
||
def attach_notifications(target, features) -> None:
|
||
calls.append("notifications")
|
||
target.window_notification_center = object()
|
||
|
||
def build_page_actions(target) -> object:
|
||
calls.append("page_actions")
|
||
self.assertTrue(hasattr(target, "window_notification_center"))
|
||
return page_actions
|
||
|
||
with (
|
||
patch.object(window_runtime_setup, "attach_window_lifecycle", side_effect=lambda *_: calls.append("lifecycle")),
|
||
patch.object(window_runtime_setup, "attach_window_notifications", side_effect=attach_notifications),
|
||
patch.object(window_runtime_setup, "attach_startup_deps_to_window", side_effect=lambda *_: calls.append("startup") or SimpleNamespace(continue_deferred_init=object())),
|
||
patch.object(window_runtime_setup, "attach_window_ui_root", side_effect=lambda *args, **kwargs: calls.append("ui_root")),
|
||
patch.object(window_runtime_setup, "restore_window_geometry", side_effect=lambda *_: calls.append("geometry")),
|
||
patch.object(window_runtime_setup, "connect_window_startup_signals", side_effect=lambda *args, **kwargs: calls.append("signals")),
|
||
patch.object(window_runtime_setup, "show_initial_window_if_needed", side_effect=lambda *_: calls.append("show")),
|
||
patch.object(window_runtime_setup, "start_window_deferred_init", side_effect=lambda *_: calls.append("deferred")),
|
||
):
|
||
window_runtime_setup.attach_app_runtime_to_window(
|
||
window,
|
||
app_runtime,
|
||
page_actions_factory=build_page_actions,
|
||
)
|
||
|
||
self.assertEqual(
|
||
calls,
|
||
[
|
||
"lifecycle",
|
||
"notifications",
|
||
"page_actions",
|
||
"startup",
|
||
"ui_root",
|
||
"geometry",
|
||
"signals",
|
||
"show",
|
||
"deferred",
|
||
],
|
||
)
|
||
|
||
def test_main_defers_late_bootstrap_until_qt_event_loop_runs(self) -> None:
|
||
from main import entry
|
||
import main.application_controller as application_controller_module
|
||
import main.window as window_module
|
||
import main.windows_session_shutdown as shutdown_module
|
||
|
||
calls: list[str] = []
|
||
scheduled: list[tuple[int, object]] = []
|
||
timer_delays: list[int] = []
|
||
app_runtime = object()
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(value)
|
||
|
||
startup_signal = Signal()
|
||
window = SimpleNamespace(
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
startup_interactive_ready=startup_signal,
|
||
)
|
||
|
||
class App:
|
||
def exec(self) -> int:
|
||
calls.append("app.exec")
|
||
while scheduled:
|
||
_delay, callback = scheduled.pop(0)
|
||
callback()
|
||
calls.append("startup_interactive.emit")
|
||
window.startup_state.interactive_logged = True
|
||
startup_signal.emit("ui_ready")
|
||
while scheduled:
|
||
_delay, callback = scheduled.pop(0)
|
||
callback()
|
||
return 0
|
||
|
||
class Controller:
|
||
def __init__(self, *, window_cls, start_in_tray) -> None:
|
||
calls.append("controller.init")
|
||
self.app_runtime = app_runtime
|
||
self.window_state_actions = object()
|
||
|
||
def create_window(self):
|
||
calls.append("create_window")
|
||
return window
|
||
|
||
class Bridge:
|
||
def __init__(self, target_window) -> None:
|
||
pass
|
||
|
||
def start(self) -> bool:
|
||
calls.append("bridge.start")
|
||
return True
|
||
|
||
with (
|
||
patch.object(entry, "require_packaged_application"),
|
||
patch.object(entry, "shell_bootstrap", return_value=False),
|
||
patch.object(entry, "application_bootstrap", return_value=App()),
|
||
patch.object(entry, "is_qt_event_diagnostic_enabled", return_value=False),
|
||
patch.object(application_controller_module, "ApplicationController", Controller),
|
||
patch.object(window_module, "LupiDPIApp", object()),
|
||
patch.object(shutdown_module, "connect_windows_session_shutdown", side_effect=lambda *_: calls.append("shutdown.hook")),
|
||
patch.object(entry, "_configure_window_appearance", side_effect=lambda *_: calls.append("appearance")),
|
||
patch("startup.show_window_bridge.ShowWindowBridge", Bridge),
|
||
patch.object(
|
||
entry.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay, callback: (
|
||
timer_delays.append(int(delay)),
|
||
calls.append("schedule.late_bootstrap"),
|
||
scheduled.append((delay, callback)),
|
||
),
|
||
),
|
||
patch.object(entry, "_build_application_post_startup_deps", side_effect=lambda **_: calls.append("build_post_startup_deps") or object()),
|
||
patch.object(entry, "_install_post_startup_tasks", side_effect=lambda *_: calls.append("install_post_startup")),
|
||
):
|
||
with self.assertRaises(SystemExit):
|
||
entry.main()
|
||
|
||
self.assertEqual(timer_delays[0], 0)
|
||
self.assertLess(calls.index("schedule.late_bootstrap"), calls.index("app.exec"))
|
||
self.assertLess(calls.index("app.exec"), calls.index("install_post_startup"))
|
||
self.assertLess(calls.index("startup_interactive.emit"), calls.index("install_post_startup"))
|
||
self.assertLess(calls.index("appearance"), calls.index("install_post_startup"))
|
||
self.assertLess(calls.index("bridge.start"), calls.index("install_post_startup"))
|
||
|
||
def test_post_startup_install_is_bound_to_interactive_ready(self) -> None:
|
||
from main import entry
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(value)
|
||
|
||
signal = Signal()
|
||
window = SimpleNamespace(
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
startup_interactive_ready=signal,
|
||
)
|
||
deps = object()
|
||
scheduled: list[tuple[int, object]] = []
|
||
installed: list[object] = []
|
||
|
||
with (
|
||
patch.object(
|
||
entry.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay, callback: scheduled.append((int(delay), callback)),
|
||
),
|
||
patch.object(entry, "_install_post_startup_tasks", side_effect=lambda value: installed.append(value)),
|
||
):
|
||
entry._install_post_startup_tasks_after_interactive(window, deps)
|
||
self.assertEqual(installed, [])
|
||
self.assertEqual(scheduled, [])
|
||
|
||
window.startup_state.interactive_logged = True
|
||
signal.emit("ui_ready")
|
||
self.assertEqual(installed, [])
|
||
self.assertEqual(scheduled[0][0], 0)
|
||
scheduled.pop(0)[1]()
|
||
|
||
self.assertEqual(installed, [deps])
|
||
|
||
def test_post_startup_deps_are_built_after_interactive_ready(self) -> None:
|
||
from main import entry
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(value)
|
||
|
||
signal = Signal()
|
||
window = SimpleNamespace(
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
startup_interactive_ready=signal,
|
||
)
|
||
built: list[str] = []
|
||
installed: list[object] = []
|
||
scheduled: list[tuple[int, object]] = []
|
||
|
||
def build_deps():
|
||
built.append("deps")
|
||
return "deps"
|
||
|
||
with (
|
||
patch.object(
|
||
entry.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay, callback: scheduled.append((int(delay), callback)),
|
||
),
|
||
patch.object(entry, "_install_post_startup_tasks", side_effect=lambda deps: installed.append(deps)),
|
||
):
|
||
entry._install_post_startup_tasks_after_interactive(window, build_deps)
|
||
self.assertEqual(built, [])
|
||
self.assertEqual(installed, [])
|
||
|
||
window.startup_state.interactive_logged = True
|
||
signal.emit("ui_ready")
|
||
self.assertEqual(built, ["deps"])
|
||
self.assertEqual(installed, [])
|
||
|
||
scheduled.pop(0)[1]()
|
||
|
||
self.assertEqual(installed, ["deps"])
|
||
|
||
def test_qt_scroll_style_is_installed_after_interactive_ready(self) -> None:
|
||
from main import entry
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(value)
|
||
|
||
signal = Signal()
|
||
window = SimpleNamespace(
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
startup_interactive_ready=signal,
|
||
)
|
||
app = object()
|
||
installed: list[object] = []
|
||
scheduled: list[tuple[int, object]] = []
|
||
|
||
with (
|
||
patch.object(
|
||
entry.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay, callback: scheduled.append((int(delay), callback)),
|
||
),
|
||
patch.object(entry, "_install_qt_scroll_style", side_effect=lambda value: installed.append(value)),
|
||
):
|
||
entry._install_qt_scroll_style_after_interactive(window, app)
|
||
self.assertEqual(installed, [])
|
||
self.assertEqual(scheduled, [])
|
||
|
||
window.startup_state.interactive_logged = True
|
||
signal.emit("ui_ready")
|
||
self.assertEqual(installed, [])
|
||
self.assertGreaterEqual(scheduled[0][0], 1500)
|
||
|
||
scheduled.pop(0)[1]()
|
||
|
||
self.assertEqual(installed, [app])
|
||
|
||
def test_entry_keeps_post_startup_imports_out_of_top_level(self) -> None:
|
||
import inspect
|
||
from main import entry
|
||
|
||
source = inspect.getsource(entry)
|
||
top_level = source.split("def _build_application_post_startup_deps", 1)[0]
|
||
|
||
self.assertNotIn("from startup.show_window_bridge import ShowWindowBridge", top_level)
|
||
self.assertNotIn("from main.application_post_startup import build_application_post_startup_deps", top_level)
|
||
self.assertNotIn("from main.post_startup import install_post_startup_tasks", top_level)
|
||
|
||
def test_post_startup_module_keeps_task_imports_lazy(self) -> None:
|
||
import inspect
|
||
from main import post_startup
|
||
|
||
source = inspect.getsource(post_startup)
|
||
top_level = source.split("def install_startup_checks", 1)[0]
|
||
|
||
self.assertNotIn("from main.post_startup_checks import", top_level)
|
||
self.assertNotIn("from main.post_startup_dns_warmup import", top_level)
|
||
self.assertNotIn("from main.post_startup_page_warmup import", top_level)
|
||
self.assertNotIn("from main.post_startup_user_presets_warmup import", top_level)
|
||
self.assertNotIn("from main.post_startup_update import", top_level)
|
||
|
||
def test_startup_audit_is_disabled_by_default_after_temporary_diagnostics(self) -> None:
|
||
from main import startup_audit
|
||
|
||
with patch.dict("os.environ", {}, clear=True), patch.object(sys, "argv", ["Zapret.exe"]):
|
||
self.assertFalse(startup_audit.is_startup_audit_enabled())
|
||
|
||
with patch.dict("os.environ", {"ZAPRET_STARTUP_AUDIT": "0"}, clear=True):
|
||
self.assertFalse(startup_audit.is_startup_audit_enabled())
|
||
|
||
with patch.dict("os.environ", {}, clear=True), patch.object(sys, "argv", ["Zapret.exe", "--startup-audit"]):
|
||
self.assertTrue(startup_audit.is_startup_audit_enabled())
|
||
|
||
def test_startup_metric_includes_memory_when_audit_is_enabled(self) -> None:
|
||
from main import runtime_state
|
||
|
||
with (
|
||
patch("log.log.log") as log_fn,
|
||
patch("main.startup_audit.is_startup_audit_enabled", return_value=True),
|
||
patch("main.startup_audit.process_memory_details", return_value="rss=123.4MB"),
|
||
):
|
||
runtime_state.log_startup_metric("DemoMetric", "details")
|
||
|
||
logged_text = str(log_fn.call_args.args[0])
|
||
self.assertIn("DemoMetric", logged_text)
|
||
self.assertIn("details", logged_text)
|
||
self.assertIn("rss=123.4MB", logged_text)
|
||
|
||
def test_startup_threading_reports_audit_boundaries(self) -> None:
|
||
from main import post_startup_threading
|
||
|
||
calls: list[str] = []
|
||
|
||
with (
|
||
patch.object(post_startup_threading.startup_audit, "is_startup_audit_enabled", return_value=True),
|
||
patch.object(post_startup_threading.startup_audit, "audit_task_begin", side_effect=lambda name, kind: calls.append(f"begin:{kind}:{name}") or 11),
|
||
patch.object(post_startup_threading.startup_audit, "audit_task_end", side_effect=lambda task_id, name, kind: calls.append(f"end:{kind}:{name}:{task_id}")),
|
||
):
|
||
post_startup_threading._run_audited_target("DemoWorker", lambda: calls.append("target"))
|
||
|
||
self.assertEqual(calls, ["begin:thread:DemoWorker", "target", "end:thread:DemoWorker:11"])
|
||
|
||
def test_startup_threading_uses_separate_serial_queues_per_subsystem(self) -> None:
|
||
import threading
|
||
import time
|
||
from main import post_startup_threading
|
||
|
||
queue_suffix = str(time.monotonic_ns())
|
||
calls: list[str] = []
|
||
first_started = threading.Event()
|
||
release_first = threading.Event()
|
||
second_done = threading.Event()
|
||
other_done = threading.Event()
|
||
|
||
def first_hosts_task() -> None:
|
||
calls.append("hosts:first:start")
|
||
first_started.set()
|
||
release_first.wait(2)
|
||
calls.append("hosts:first:end")
|
||
|
||
def second_hosts_task() -> None:
|
||
calls.append("hosts:second")
|
||
second_done.set()
|
||
|
||
def profile_task() -> None:
|
||
calls.append("profile:first")
|
||
other_done.set()
|
||
|
||
post_startup_threading.enqueue_subsystem_task(
|
||
f"hosts-{queue_suffix}",
|
||
"HostsWarmup-1",
|
||
first_hosts_task,
|
||
)
|
||
post_startup_threading.enqueue_subsystem_task(
|
||
f"hosts-{queue_suffix}",
|
||
"HostsWarmup-2",
|
||
second_hosts_task,
|
||
)
|
||
post_startup_threading.enqueue_subsystem_task(
|
||
f"profile-{queue_suffix}",
|
||
"ProfileWarmup-1",
|
||
profile_task,
|
||
)
|
||
|
||
self.assertTrue(first_started.wait(1.0))
|
||
self.assertTrue(other_done.wait(1.0))
|
||
self.assertFalse(second_done.wait(0.05))
|
||
self.assertIn("profile:first", calls)
|
||
|
||
release_first.set()
|
||
self.assertTrue(second_done.wait(1.0))
|
||
self.assertLess(calls.index("hosts:first:start"), calls.index("hosts:first:end"))
|
||
self.assertLess(calls.index("hosts:first:end"), calls.index("hosts:second"))
|
||
self.assertLess(calls.index("profile:first"), calls.index("hosts:first:end"))
|
||
|
||
def test_startup_audit_summary_installed_by_post_startup_tasks(self) -> None:
|
||
from main import post_startup
|
||
|
||
deps = SimpleNamespace(
|
||
startup_host=object(),
|
||
hosts_feature=object(),
|
||
profile_feature=object(),
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=Mock(),
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=object(),
|
||
premium_feature=None,
|
||
logs_feature=None,
|
||
presets_feature=None,
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
patch.object(post_startup, "install_startup_audit") as install_audit,
|
||
):
|
||
post_startup.install_post_startup_tasks(deps)
|
||
|
||
install_audit.assert_called_once()
|
||
|
||
def test_startup_log_contract_accepts_ui_before_runtime_order(self) -> None:
|
||
from main.startup_log_contract import validate_startup_log_contract
|
||
|
||
result = validate_startup_log_contract(
|
||
"\n".join(
|
||
(
|
||
"[12:00:00] [⏱ STARTUP] ⏱ Startup StartupTTFF: 1000ms | first showEvent",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupInteractive: 1200ms | ui_ready",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupRuntimeInitQueued: 1450ms | 25ms gaps",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupCoreReady: 1800ms | startup_coordinator",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupPostInit: 1900ms | post_init_scheduled",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupPostInitDeferredStart: 2300ms | zapret2_mode",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupNetworkDataWarmupQueued: 2400ms | 1200ms after interactive",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupSidebarSearchQueued: 2500ms | 1000ms after interactive",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupHiddenModeNavQueued: 2800ms | 1600ms after interactive",
|
||
)
|
||
)
|
||
)
|
||
|
||
self.assertTrue(result.ok, result.errors)
|
||
|
||
def test_startup_log_contract_accepts_hidden_tray_start_before_first_show(self) -> None:
|
||
from main.startup_log_contract import validate_startup_log_contract
|
||
|
||
result = validate_startup_log_contract(
|
||
"\n".join(
|
||
(
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupInteractive: 5400ms | ui_ready",
|
||
"[12:00:01] [TRAY] Запуск приложения скрыто в трее",
|
||
"[12:00:02] [⏱ STARTUP] ⏱ Startup StartupRuntimeInitQueued: 5500ms | 0ms gaps",
|
||
"[12:00:03] [⏱ STARTUP] ⏱ Startup StartupCoreReady: 6400ms | startup_coordinator",
|
||
"[12:00:03] [⏱ STARTUP] ⏱ Startup StartupPostInit: 6500ms | post_init_scheduled",
|
||
"[12:05:49] [⏱ STARTUP] ⏱ Startup StartupTTFF: 352855ms | first showEvent",
|
||
)
|
||
)
|
||
)
|
||
|
||
self.assertTrue(result.ok, result.errors)
|
||
|
||
def test_startup_log_contract_rejects_runtime_before_interactive(self) -> None:
|
||
from main.startup_log_contract import validate_startup_log_contract
|
||
|
||
result = validate_startup_log_contract(
|
||
"\n".join(
|
||
(
|
||
"[12:00:00] [⏱ STARTUP] ⏱ Startup StartupTTFF: 1000ms | first showEvent",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupRuntimeInitQueued: 1100ms | 25ms gaps",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupInteractive: 1300ms | ui_ready",
|
||
)
|
||
)
|
||
)
|
||
|
||
self.assertFalse(result.ok)
|
||
self.assertTrue(any("StartupRuntimeInitQueued" in error for error in result.errors))
|
||
|
||
def test_startup_log_contract_rejects_gui_page_warmup_during_startup(self) -> None:
|
||
from main.startup_log_contract import validate_startup_log_contract
|
||
|
||
result = validate_startup_log_contract(
|
||
"\n".join(
|
||
(
|
||
"[12:00:00] [⏱ STARTUP] ⏱ Startup StartupTTFF: 1000ms | first showEvent",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupInteractive: 1300ms | ui_ready",
|
||
"[12:00:10] [⏱ UI] ⏱ UiMetric: scope=page name=NETWORK stage=warmup elapsed=237ms",
|
||
)
|
||
)
|
||
)
|
||
|
||
self.assertFalse(result.ok)
|
||
self.assertTrue(any("GUI-прогрев страницы" in error for error in result.errors))
|
||
|
||
def test_startup_log_contract_rejects_startup_page_warmup_queue(self) -> None:
|
||
from main.startup_log_contract import validate_startup_log_contract
|
||
|
||
result = validate_startup_log_contract(
|
||
"\n".join(
|
||
(
|
||
"[12:00:00] [⏱ STARTUP] ⏱ Startup StartupTTFF: 1000ms | first showEvent",
|
||
"[12:00:01] [⏱ STARTUP] ⏱ Startup StartupInteractive: 1300ms | ui_ready",
|
||
"[12:00:10] [⏱ STARTUP] ⏱ Startup StartupPageNetworkWarmupQueued: 9000ms | NETWORK",
|
||
)
|
||
)
|
||
)
|
||
|
||
self.assertFalse(result.ok)
|
||
self.assertTrue(any("Во время запуска можно греть только данные" in error for error in result.errors))
|
||
|
||
def test_startup_runtime_step_gap_stays_short_after_interactive(self) -> None:
|
||
from main import startup_coordinator
|
||
|
||
self.assertLessEqual(startup_coordinator.STARTUP_STEP_GAP_MS, 8)
|
||
|
||
def test_dns_feature_exposes_startup_dns_entrypoint(self) -> None:
|
||
from app.feature_facades.dns import build_dns_feature
|
||
|
||
dns_feature = build_dns_feature()
|
||
|
||
self.assertTrue(callable(dns_feature.apply_dns_on_startup_async))
|
||
self.assertTrue(callable(dns_feature.warm_page_data_cache))
|
||
self.assertTrue(callable(dns_feature.consume_warmed_page_data))
|
||
|
||
def test_window_startup_metrics_are_imported_for_interactive_ready(self) -> None:
|
||
import main.window_startup as window_startup
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self.emitted: list[str] = []
|
||
|
||
def emit(self, value: str) -> None:
|
||
self.emitted.append(value)
|
||
|
||
window = object.__new__(window_startup.WindowStartupMixin)
|
||
window.startup_state = SimpleNamespace(
|
||
interactive_logged=False,
|
||
interactive_ms=None,
|
||
ttff_ms=None,
|
||
)
|
||
window.startup_interactive_ready = Signal()
|
||
|
||
with patch.object(window_startup, "emit_startup_metric") as metric:
|
||
window.mark_startup_interactive("test_ready")
|
||
|
||
metric.assert_called_once_with("StartupInteractive", "test_ready")
|
||
self.assertEqual(window.startup_interactive_ready.emitted, ["test_ready"])
|
||
|
||
def test_initial_manual_launch_state_keeps_runtime_buttons_preparing(self) -> None:
|
||
from app.feature_facades.runtime_parts import RuntimeObjects
|
||
from winws_runtime.state import LaunchRuntimeService
|
||
|
||
_ = RuntimeObjects
|
||
state = LaunchRuntimeService.build_initial_ui_state(
|
||
launch_method="zapret2_mode",
|
||
dpi_autostart_enabled=False,
|
||
launch_supported=True,
|
||
)
|
||
|
||
self.assertEqual(state.launch_phase, "stopped")
|
||
self.assertTrue(state.launch_busy)
|
||
self.assertIn("Подготов", state.launch_busy_text)
|
||
|
||
def test_initial_autostart_state_uses_autostart_pending_without_extra_busy_flag(self) -> None:
|
||
from app.feature_facades.runtime_parts import RuntimeObjects
|
||
from winws_runtime.state import LaunchRuntimeService
|
||
|
||
_ = RuntimeObjects
|
||
state = LaunchRuntimeService.build_initial_ui_state(
|
||
launch_method="zapret2_mode",
|
||
dpi_autostart_enabled=True,
|
||
launch_supported=True,
|
||
)
|
||
|
||
self.assertEqual(state.launch_phase, "autostart_pending")
|
||
self.assertFalse(state.launch_busy)
|
||
self.assertEqual(state.launch_busy_text, "")
|
||
|
||
def test_runtime_init_clears_startup_button_preparation(self) -> None:
|
||
from app.feature_facades.runtime_parts import RuntimeCommandPort
|
||
|
||
runtime_object = object()
|
||
runtime_service = SimpleNamespace(set_busy=Mock())
|
||
owner = SimpleNamespace(
|
||
objects=SimpleNamespace(
|
||
launch_runtime_api=object(),
|
||
launch_runtime=None,
|
||
runtime_service=runtime_service,
|
||
),
|
||
events=SimpleNamespace(ensure_dispatcher=Mock()),
|
||
ui_port=SimpleNamespace(require_notifications=Mock(return_value=object())),
|
||
)
|
||
commands = RuntimeCommandPort(owner)
|
||
runtime_commands = SimpleNamespace(init_launch_runtime=Mock(return_value=runtime_object))
|
||
|
||
with patch.object(RuntimeCommandPort, "_runtime_commands", staticmethod(lambda: runtime_commands)):
|
||
commands.init_launch_runtime()
|
||
|
||
self.assertIs(owner.objects.launch_runtime, runtime_object)
|
||
owner.events.ensure_dispatcher.assert_called_once_with()
|
||
runtime_service.set_busy.assert_called_once_with(False)
|
||
|
||
def test_runtime_api_init_failure_clears_startup_button_preparation(self) -> None:
|
||
from app.feature_facades.runtime_parts import RuntimeCommandPort
|
||
|
||
runtime_service = SimpleNamespace(set_busy=Mock(), mark_start_failed=Mock())
|
||
owner = SimpleNamespace(
|
||
objects=SimpleNamespace(
|
||
launch_runtime_api=None,
|
||
runtime_service=runtime_service,
|
||
),
|
||
)
|
||
commands = RuntimeCommandPort(owner)
|
||
runtime_commands = SimpleNamespace(init_launch_runtime_api=Mock(side_effect=RuntimeError("boom")))
|
||
|
||
with patch.object(RuntimeCommandPort, "_runtime_commands", staticmethod(lambda: runtime_commands)):
|
||
with self.assertRaises(RuntimeError):
|
||
commands.init_launch_runtime_api()
|
||
|
||
runtime_service.set_busy.assert_called_once_with(False)
|
||
runtime_service.mark_start_failed.assert_called_once_with("boom")
|
||
|
||
def test_runtime_init_failure_clears_startup_button_preparation(self) -> None:
|
||
from app.feature_facades.runtime_parts import RuntimeCommandPort
|
||
|
||
runtime_service = SimpleNamespace(set_busy=Mock(), mark_start_failed=Mock())
|
||
owner = SimpleNamespace(
|
||
objects=SimpleNamespace(
|
||
launch_runtime_api=object(),
|
||
launch_runtime=None,
|
||
runtime_service=runtime_service,
|
||
),
|
||
events=SimpleNamespace(ensure_dispatcher=Mock()),
|
||
ui_port=SimpleNamespace(require_notifications=Mock(return_value=object())),
|
||
)
|
||
commands = RuntimeCommandPort(owner)
|
||
runtime_commands = SimpleNamespace(init_launch_runtime=Mock(side_effect=RuntimeError("boom")))
|
||
|
||
with patch.object(RuntimeCommandPort, "_runtime_commands", staticmethod(lambda: runtime_commands)):
|
||
with self.assertRaises(RuntimeError):
|
||
commands.init_launch_runtime()
|
||
|
||
runtime_service.set_busy.assert_called_once_with(False)
|
||
runtime_service.mark_start_failed.assert_called_once_with("boom")
|
||
|
||
def test_runtime_feature_assembly_uses_light_state_import_before_ui(self) -> None:
|
||
import inspect
|
||
from app.feature_facades import runtime as runtime_feature_module
|
||
from app.feature_facades import runtime_parts
|
||
|
||
runtime_source = inspect.getsource(runtime_feature_module)
|
||
runtime_parts_source = inspect.getsource(runtime_parts)
|
||
|
||
self.assertIn("from winws_runtime.state import LaunchRuntimeService", runtime_source)
|
||
self.assertNotIn("from winws_runtime.public import LaunchRuntimeService", runtime_source)
|
||
self.assertNotIn("import winws_runtime.public as runtime_commands", runtime_parts_source)
|
||
|
||
def test_non_initial_feature_facades_do_not_import_heavy_command_modules_before_ui(self) -> None:
|
||
import inspect
|
||
from app.feature_facades import (
|
||
blockcheck,
|
||
logs,
|
||
orchestra,
|
||
premium,
|
||
presets,
|
||
profile,
|
||
program_settings,
|
||
tray,
|
||
updater,
|
||
)
|
||
|
||
blocked_top_level_imports = {
|
||
blockcheck: (
|
||
"import blockcheck.public as blockcheck_commands",
|
||
"import blockcheck.page_runtime as blockcheck_page_runtime",
|
||
"from blockcheck import commands as blockcheck_worker_commands",
|
||
),
|
||
logs: (
|
||
"import log.commands as log_commands",
|
||
"import log.runtime_workflow as log_runtime",
|
||
),
|
||
orchestra: (
|
||
"import orchestra.commands as orchestra_commands",
|
||
),
|
||
premium: (
|
||
"import donater.commands as premium_commands",
|
||
),
|
||
presets: (
|
||
"import presets.commands as preset_commands",
|
||
"import presets.display_state as preset_display",
|
||
),
|
||
profile: (
|
||
"from profile import commands as profile_internal_commands",
|
||
"from profile import settings as profile_settings",
|
||
),
|
||
program_settings: (
|
||
"import program_settings.public as program_settings_commands",
|
||
),
|
||
tray: (
|
||
"import tray_commands",
|
||
),
|
||
updater: (
|
||
"import updater.public as updater_commands",
|
||
),
|
||
}
|
||
|
||
for module, forbidden_imports in blocked_top_level_imports.items():
|
||
source = inspect.getsource(module)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
for import_line in forbidden_imports:
|
||
self.assertNotIn(import_line, top_level)
|
||
|
||
def test_tray_window_port_defers_heavy_ui_helpers(self) -> None:
|
||
import inspect
|
||
import main.tray_window_port as tray_window_port
|
||
|
||
source = inspect.getsource(tray_window_port)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertNotIn("from ui.popup_menu import", top_level)
|
||
self.assertNotIn("from qfluentwidgets import", top_level)
|
||
self.assertNotIn("from log.log import", top_level)
|
||
self.assertIn("from ui.window_adapter import show_window", inspect.getsource(tray_window_port.TrayWindowPort.show))
|
||
self.assertIn("from ui.popup_menu import exec_popup_menu", inspect.getsource(tray_window_port.TrayWindowPort.exec_popup_menu))
|
||
|
||
def test_window_page_actions_defers_heavy_ui_helpers(self) -> None:
|
||
import inspect
|
||
import main.window_page_actions as window_page_actions
|
||
|
||
source = inspect.getsource(window_page_actions)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertNotIn("from ui.workflows.mode import", top_level)
|
||
self.assertNotIn("from ui.window_appearance_state import", top_level)
|
||
self.assertNotIn("from ui.navigation.text_sync import", top_level)
|
||
self.assertNotIn("from main.window_page_presenters import", top_level)
|
||
self.assertIn("from ui.window_adapter import show_page", inspect.getsource(window_page_actions.show_page))
|
||
self.assertIn(
|
||
"from ui.window_appearance_state import on_mica_changed",
|
||
inspect.getsource(window_page_actions.on_mica_changed),
|
||
)
|
||
|
||
def test_window_lifecycle_defers_close_cleanup_and_adapter_helpers(self) -> None:
|
||
import inspect
|
||
import main.window_lifecycle as window_lifecycle
|
||
|
||
source = inspect.getsource(window_lifecycle)
|
||
top_level = source.split("class WindowLifecycleMixin", 1)[0]
|
||
|
||
self.assertNotIn("from main.window_lifecycle_cleanup import", top_level)
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertIn("self.unsetCursor()", inspect.getsource(window_lifecycle.WindowLifecycleMixin.release_input_interaction_states))
|
||
self.assertIn(
|
||
"sync_titlebar_search_width(self)",
|
||
inspect.getsource(window_lifecycle.WindowLifecycleMixin.resizeEvent),
|
||
)
|
||
self.assertIn(
|
||
"refresh_titlebar_layout(self)",
|
||
inspect.getsource(window_lifecycle.WindowLifecycleMixin._refresh_titlebar_layout_after_show),
|
||
)
|
||
self.assertIn("from ui.window_adapter import sync_titlebar_search_width", inspect.getsource(window_lifecycle.sync_titlebar_search_width))
|
||
self.assertIn("from ui.window_adapter import refresh_titlebar_layout", inspect.getsource(window_lifecycle.refresh_titlebar_layout))
|
||
|
||
def test_application_lifecycle_defers_close_cleanup_helpers(self) -> None:
|
||
import inspect
|
||
import main.application_lifecycle as application_lifecycle
|
||
|
||
source = inspect.getsource(application_lifecycle)
|
||
top_level = source.split("class ApplicationLifecycle", 1)[0]
|
||
|
||
self.assertNotIn("from main.window_lifecycle_cleanup import", top_level)
|
||
self.assertIn(
|
||
"from main.window_lifecycle_cleanup import detach_global_error_notifier",
|
||
inspect.getsource(application_lifecycle.ApplicationLifecycle.run_final_close_cleanup),
|
||
)
|
||
self.assertIn(
|
||
"from main.window_lifecycle_cleanup import",
|
||
inspect.getsource(application_lifecycle.ApplicationLifecycle._cleanup_before_close),
|
||
)
|
||
|
||
def test_application_lifecycle_port_defers_close_cleanup_helpers(self) -> None:
|
||
import inspect
|
||
import main.application_lifecycle_port as application_lifecycle_port
|
||
|
||
source = inspect.getsource(application_lifecycle_port)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from main.window_lifecycle_cleanup import", top_level)
|
||
self.assertIn(
|
||
"from main.window_lifecycle_cleanup import persist_window_geometry",
|
||
inspect.getsource(application_lifecycle_port.ApplicationLifecycleWindowPort.persist_geometry),
|
||
)
|
||
self.assertIn(
|
||
"cleanup_threaded_pages_for_close",
|
||
inspect.getsource(application_lifecycle_port.ApplicationLifecycleWindowPort.cleanup_threaded_pages),
|
||
)
|
||
|
||
def test_window_notifications_defers_page_adapter(self) -> None:
|
||
import inspect
|
||
import main.window_notifications_setup as window_notifications_setup
|
||
|
||
source = inspect.getsource(window_notifications_setup)
|
||
top_level = source.split("def show_page", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertIn(
|
||
"from ui.window_adapter import show_page",
|
||
inspect.getsource(window_notifications_setup.show_page),
|
||
)
|
||
|
||
def test_window_actions_defers_window_adapter(self) -> None:
|
||
import inspect
|
||
import main.window_actions as window_actions
|
||
|
||
source = inspect.getsource(window_actions)
|
||
top_level = source.split("class WindowActionsMixin", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertNotIn("from ui.page_actions import", top_level)
|
||
self.assertIn(
|
||
"from ui.window_adapter import route_window_search_result, show_page",
|
||
inspect.getsource(window_actions.WindowActionsMixin.open_connection_test),
|
||
)
|
||
|
||
def test_window_runtime_setup_binds_open_folder_worker_factory(self) -> None:
|
||
import inspect
|
||
import main.window_runtime_setup as window_runtime_setup
|
||
|
||
attach_source = inspect.getsource(window_runtime_setup.attach_app_runtime_to_window)
|
||
factory_source = inspect.getsource(window_runtime_setup.create_open_folder_worker)
|
||
|
||
self.assertIn("bind_open_folder_worker_factory", attach_source)
|
||
self.assertIn("create_open_folder_worker", attach_source)
|
||
self.assertIn("WindowOpenFolderWorker", factory_source)
|
||
self.assertIn("open_program_folder", factory_source)
|
||
|
||
def test_window_feature_deps_defers_appearance_bindings(self) -> None:
|
||
import inspect
|
||
import main.window_feature_deps as window_feature_deps
|
||
|
||
source = inspect.getsource(window_feature_deps)
|
||
top_level = source.split("def initialize_window_holiday_effects", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_appearance_bindings import", top_level)
|
||
self.assertIn(
|
||
"from ui.window_appearance_bindings import initialize_window_holiday_effects",
|
||
inspect.getsource(window_feature_deps.initialize_window_holiday_effects),
|
||
)
|
||
|
||
def test_feature_assembly_defers_feature_facades_import(self) -> None:
|
||
import inspect
|
||
import app.feature_assembly as feature_assembly
|
||
|
||
source = inspect.getsource(feature_assembly)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from app.feature_facades import", top_level)
|
||
self.assertIn(
|
||
"from app.feature_facades.presets import PresetsFeature",
|
||
inspect.getsource(feature_assembly.build_preset_profile_features),
|
||
)
|
||
self.assertIn(
|
||
"from app.feature_facades.profile import ProfileFeature",
|
||
inspect.getsource(feature_assembly.build_preset_profile_features),
|
||
)
|
||
# Фасады импортируются лениво внутри build_app_features через
|
||
# _timed_facade_import, чтобы замерять стоимость каждого импорта.
|
||
build_source = inspect.getsource(feature_assembly.build_app_features)
|
||
self.assertIn("_timed_facade_import", build_source)
|
||
self.assertIn('"blockcheck"', build_source)
|
||
self.assertNotIn(
|
||
"from app.feature_facades import (",
|
||
build_source,
|
||
)
|
||
timed_import_source = inspect.getsource(feature_assembly._timed_facade_import)
|
||
self.assertIn('import_module(f"app.feature_facades.{facade_name}")', timed_import_source)
|
||
|
||
def test_page_deps_presets_defers_preset_page_imports(self) -> None:
|
||
import inspect
|
||
import ui.page_deps.presets as page_deps_presets
|
||
|
||
source = inspect.getsource(page_deps_presets)
|
||
top_level = source.split("def build_control_page_kwargs", 1)[0]
|
||
|
||
# preset_subpage_base тянет ui.fluent_widgets/qtawesome (~200 мс),
|
||
# а этот модуль импортируется до первого показа окна.
|
||
self.assertNotIn("from presets.ui.", top_level)
|
||
self.assertIn(
|
||
"from presets.ui.common.preset_subpage_base import RawPresetRuntimeActions",
|
||
inspect.getsource(page_deps_presets.build_preset_raw_editor_page_kwargs),
|
||
)
|
||
self.assertIn(
|
||
"from presets.ui.common.user_presets_page_runtime import UserPresetsRuntimeActions",
|
||
inspect.getsource(page_deps_presets.build_user_presets_page_kwargs),
|
||
)
|
||
self.assertIn(
|
||
"from presets.ui.control.control_page_shared import ControlRuntimeActions",
|
||
inspect.getsource(page_deps_presets.build_control_page_kwargs),
|
||
)
|
||
|
||
def test_app_features_defers_feature_facades_imports_to_type_checking(self) -> None:
|
||
import inspect
|
||
import app.features as app_features
|
||
|
||
source = inspect.getsource(app_features)
|
||
before_type_checking = source.split("if TYPE_CHECKING:", 1)[0]
|
||
|
||
self.assertNotIn("from app.feature_facades import", before_type_checking)
|
||
self.assertIn("if TYPE_CHECKING:", source)
|
||
self.assertIn("from app.feature_facades import", source)
|
||
|
||
def test_window_state_actions_defers_appearance_state_helpers(self) -> None:
|
||
import inspect
|
||
import main.window_state_actions as window_state_actions
|
||
|
||
source = inspect.getsource(window_state_actions)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_appearance_state import", top_level)
|
||
self.assertNotIn("from log.log import", top_level)
|
||
self.assertIn(
|
||
"from ui.window_appearance_state import apply_window_opacity_value",
|
||
inspect.getsource(window_state_actions.WindowStateActions.set_window_opacity),
|
||
)
|
||
|
||
def test_window_page_deps_setup_defers_ui_root_imports(self) -> None:
|
||
import inspect
|
||
import main.window_page_deps_setup as window_page_deps_setup
|
||
|
||
source = inspect.getsource(window_page_deps_setup)
|
||
top_level = source.split("def build_window_page_deps_sources", 1)[0]
|
||
|
||
self.assertNotIn("from ui.page_deps.common import", top_level)
|
||
self.assertNotIn("from ui.ui_root import", top_level)
|
||
self.assertNotIn("from ui.window_bootstrap_runtime import", top_level)
|
||
self.assertIn(
|
||
"from ui.page_deps.common import PageDepsSources",
|
||
inspect.getsource(window_page_deps_setup.build_window_page_deps_sources),
|
||
)
|
||
self.assertIn(
|
||
"from ui.ui_root import WindowUiRoot",
|
||
inspect.getsource(window_page_deps_setup.attach_window_ui_root),
|
||
)
|
||
|
||
def test_post_startup_host_defers_window_adapter(self) -> None:
|
||
import inspect
|
||
import main.post_startup_host as post_startup_host
|
||
|
||
source = inspect.getsource(post_startup_host)
|
||
top_level = source.split("@dataclass", 1)[0]
|
||
|
||
self.assertNotIn("from ui.window_adapter import", top_level)
|
||
self.assertIn(
|
||
"from ui.window_adapter import show_page",
|
||
inspect.getsource(post_startup_host.PostStartupHost.show_page),
|
||
)
|
||
self.assertIn(
|
||
"from ui.window_adapter import get_loaded_page",
|
||
inspect.getsource(post_startup_host.PostStartupHost.get_loaded_page),
|
||
)
|
||
|
||
def test_lazy_feature_facades_still_call_loaded_command_modules(self) -> None:
|
||
from app.feature_facades.blockcheck import BlockcheckFeature
|
||
from app.feature_facades.external import ExternalActionsFeature
|
||
from app.feature_facades.logs import LogsFeature
|
||
from app.feature_facades.orchestra import OrchestraFeature
|
||
from app.feature_facades.presets import PresetsFeature
|
||
from app.feature_facades.tray import TrayFeature
|
||
from app.feature_facades.updater import UpdaterFeature
|
||
|
||
blockcheck_commands = SimpleNamespace(
|
||
build_language_plan=Mock(return_value="blockcheck-plan"),
|
||
)
|
||
logs_commands = SimpleNamespace(
|
||
build_stats=Mock(return_value="logs-stats"),
|
||
)
|
||
orchestra_commands = SimpleNamespace(
|
||
is_default_blocked_pass_domain=Mock(return_value=True),
|
||
)
|
||
preset_commands = SimpleNamespace(
|
||
get_selected_source_preset_file_name=Mock(return_value="preset.txt"),
|
||
)
|
||
tray_commands = SimpleNamespace(
|
||
show_tray_notification_if_available=Mock(return_value=True),
|
||
)
|
||
updater_commands = SimpleNamespace(
|
||
is_auto_update_enabled=Mock(return_value=True),
|
||
)
|
||
external_commands = SimpleNamespace(
|
||
open_url=Mock(return_value="opened"),
|
||
)
|
||
|
||
blockcheck_feature = BlockcheckFeature(presets_feature=object(), profile_feature=object())
|
||
external_actions_feature = ExternalActionsFeature()
|
||
logs_feature = LogsFeature()
|
||
orchestra_feature = OrchestraFeature(whitelist_runtime_service=object())
|
||
presets_feature = PresetsFeature(_services=object())
|
||
tray_feature = TrayFeature(
|
||
_deps=SimpleNamespace(),
|
||
_runtime_feature=SimpleNamespace(),
|
||
_telegram_proxy_feature=SimpleNamespace(),
|
||
_tray_manager="tray-manager",
|
||
)
|
||
updater_feature = UpdaterFeature()
|
||
|
||
with (
|
||
patch.object(BlockcheckFeature, "_commands", staticmethod(lambda: blockcheck_commands)),
|
||
patch.object(ExternalActionsFeature, "_commands", staticmethod(lambda: external_commands)),
|
||
patch.object(LogsFeature, "_commands", staticmethod(lambda: logs_commands)),
|
||
patch.object(OrchestraFeature, "_commands", staticmethod(lambda: orchestra_commands)),
|
||
patch.object(PresetsFeature, "_commands", staticmethod(lambda: preset_commands)),
|
||
patch.object(TrayFeature, "_commands", staticmethod(lambda: tray_commands)),
|
||
patch.object(UpdaterFeature, "_commands", staticmethod(lambda: updater_commands)),
|
||
):
|
||
self.assertEqual(blockcheck_feature.build_language_plan(), "blockcheck-plan")
|
||
self.assertEqual(external_actions_feature.open_url("https://example.org"), "opened")
|
||
self.assertEqual(logs_feature.build_stats(), "logs-stats")
|
||
self.assertTrue(orchestra_feature.is_default_blocked_pass_domain("example.org"))
|
||
self.assertEqual(presets_feature.get_selected_source_preset_file_name("zapret2_mode"), "preset.txt")
|
||
self.assertTrue(tray_feature.show_notification_if_available("Title", "Text"))
|
||
self.assertTrue(updater_feature.is_auto_update_enabled())
|
||
|
||
blockcheck_commands.build_language_plan.assert_called_once_with()
|
||
external_commands.open_url.assert_called_once_with("https://example.org")
|
||
logs_commands.build_stats.assert_called_once_with()
|
||
orchestra_commands.is_default_blocked_pass_domain.assert_called_once_with("example.org")
|
||
preset_commands.get_selected_source_preset_file_name.assert_called_once_with(
|
||
"zapret2_mode",
|
||
preset_services=presets_feature._services,
|
||
)
|
||
tray_commands.show_tray_notification_if_available.assert_called_once_with(
|
||
tray_manager="tray-manager",
|
||
title="Title",
|
||
content="Text",
|
||
)
|
||
updater_commands.is_auto_update_enabled.assert_called_once_with()
|
||
|
||
def test_external_actions_feature_supports_qt_signal_weakrefs(self) -> None:
|
||
from app.feature_facades.external import ExternalActionsFeature
|
||
|
||
feature = ExternalActionsFeature()
|
||
|
||
self.assertIs(weakref.ref(feature)(), feature)
|
||
|
||
def test_feature_builders_do_not_import_page_command_modules_before_ui(self) -> None:
|
||
import builtins
|
||
from app.feature_facades.diagnostics import build_diagnostics_feature
|
||
from app.feature_facades.dns import build_dns_feature
|
||
from app.feature_facades.dpi_settings import build_dpi_settings_feature
|
||
from app.feature_facades.external import build_external_actions_feature
|
||
from app.feature_facades.hosts import build_hosts_feature
|
||
from app.feature_facades.lists import build_lists_feature
|
||
from app.feature_facades.orchestra import build_orchestra_feature
|
||
from app.feature_facades.premium import build_premium_feature
|
||
from app.feature_facades.program_settings import build_program_settings_feature
|
||
from app.feature_facades.presets import PresetsFeature
|
||
from app.feature_facades.telegram_proxy import build_telegram_proxy_feature
|
||
from app.feature_facades.updater import build_updater_feature
|
||
|
||
blocked_roots = {
|
||
"diagnostics",
|
||
"dns",
|
||
"app",
|
||
"core",
|
||
"hosts",
|
||
"lists",
|
||
"orchestra",
|
||
"presets",
|
||
"donater",
|
||
"program_settings",
|
||
"telegram_proxy",
|
||
"updater",
|
||
}
|
||
imported: list[tuple[str, tuple[str, ...]]] = []
|
||
real_import = builtins.__import__
|
||
|
||
def tracking_import(name, globals=None, locals=None, fromlist=(), level=0):
|
||
root = str(name or "").split(".", 1)[0]
|
||
if root in blocked_roots:
|
||
imported.append((str(name), tuple(str(item) for item in (fromlist or ()))))
|
||
return real_import(name, globals, locals, fromlist, level)
|
||
|
||
premium_deps = SimpleNamespace(
|
||
thread_parent=object(),
|
||
set_status=Mock(),
|
||
update_title_badge=Mock(),
|
||
init_holiday_effects=Mock(),
|
||
mark_startup_ready=Mock(),
|
||
)
|
||
with patch.object(builtins, "__import__", side_effect=tracking_import):
|
||
build_diagnostics_feature()
|
||
build_dns_feature()
|
||
build_dpi_settings_feature()
|
||
build_external_actions_feature()
|
||
build_hosts_feature()
|
||
build_lists_feature()
|
||
build_orchestra_feature()
|
||
build_premium_feature(deps=premium_deps, ui_state_store=object())
|
||
build_program_settings_feature()
|
||
build_telegram_proxy_feature()
|
||
build_updater_feature()
|
||
PresetsFeature.create(object())
|
||
|
||
self.assertEqual(imported, [])
|
||
|
||
def test_orchestra_whitelist_runtime_service_is_created_lazily(self) -> None:
|
||
from app.feature_facades.orchestra import OrchestraFeature, build_orchestra_feature
|
||
|
||
created_services: list[object] = []
|
||
runtime_service = object()
|
||
commands = SimpleNamespace(
|
||
get_whitelist_snapshot=Mock(return_value="snapshot"),
|
||
)
|
||
|
||
def create_runtime_service():
|
||
created_services.append(runtime_service)
|
||
return runtime_service
|
||
|
||
with (
|
||
patch.object(OrchestraFeature, "_create_whitelist_runtime_service", staticmethod(create_runtime_service)),
|
||
patch.object(OrchestraFeature, "_commands", staticmethod(lambda: commands)),
|
||
):
|
||
feature = build_orchestra_feature()
|
||
self.assertEqual(created_services, [])
|
||
|
||
self.assertEqual(feature.get_whitelist_snapshot(None), "snapshot")
|
||
|
||
self.assertEqual(created_services, [runtime_service])
|
||
commands.get_whitelist_snapshot.assert_called_once_with(
|
||
None,
|
||
whitelist_service=runtime_service,
|
||
refresh=False,
|
||
)
|
||
|
||
def test_premium_subscription_ui_actions_are_created_lazily(self) -> None:
|
||
from app.feature_facades.premium import PremiumFeature, build_premium_feature
|
||
|
||
created_actions: list[object] = []
|
||
deps = SimpleNamespace(
|
||
thread_parent="thread-parent",
|
||
set_status=Mock(),
|
||
update_title_badge=Mock(),
|
||
init_holiday_effects=Mock(),
|
||
mark_startup_ready=Mock(),
|
||
)
|
||
ui_state_store = object()
|
||
ui_actions = object()
|
||
commands = SimpleNamespace(
|
||
create_subscription_manager=Mock(return_value="manager"),
|
||
)
|
||
|
||
def ensure_ui_actions(feature):
|
||
created_actions.append(
|
||
(
|
||
feature._deps,
|
||
feature._ui_state_store,
|
||
)
|
||
)
|
||
feature._ui_actions = ui_actions
|
||
return ui_actions
|
||
|
||
with (
|
||
patch.object(PremiumFeature, "_ensure_ui_actions", ensure_ui_actions),
|
||
patch.object(PremiumFeature, "_commands", staticmethod(lambda: commands)),
|
||
):
|
||
feature = build_premium_feature(deps=deps, ui_state_store=ui_state_store)
|
||
self.assertEqual(created_actions, [])
|
||
|
||
feature.prepare_subscription()
|
||
|
||
self.assertEqual(created_actions, [(deps, ui_state_store)])
|
||
commands.create_subscription_manager.assert_called_once_with(
|
||
thread_parent="thread-parent",
|
||
ui_actions=ui_actions,
|
||
)
|
||
|
||
def test_program_settings_runtime_service_is_created_lazily(self) -> None:
|
||
from app.feature_facades.program_settings import ProgramSettingsFeature, build_program_settings_feature
|
||
|
||
created_services: list[object] = []
|
||
runtime_service = object()
|
||
commands = SimpleNamespace(
|
||
refresh_program_settings_snapshot=Mock(return_value="snapshot"),
|
||
)
|
||
|
||
def create_runtime_service():
|
||
created_services.append(runtime_service)
|
||
return runtime_service
|
||
|
||
with (
|
||
patch.object(ProgramSettingsFeature, "_create_runtime_service", staticmethod(create_runtime_service)),
|
||
patch.object(ProgramSettingsFeature, "_commands", staticmethod(lambda: commands)),
|
||
):
|
||
feature = build_program_settings_feature()
|
||
self.assertEqual(created_services, [])
|
||
|
||
self.assertEqual(feature.refresh_program_settings_snapshot(), "snapshot")
|
||
|
||
self.assertEqual(created_services, [runtime_service])
|
||
commands.refresh_program_settings_snapshot.assert_called_once_with(runtime_service)
|
||
|
||
def test_program_settings_runtime_service_peeks_warmed_tray_close_mode_without_settings_read(self) -> None:
|
||
from core.runtime.program_settings_runtime_service import (
|
||
TRAY_CLOSE_MODE_MINIMIZE_AND_CLOSE,
|
||
ProgramSettingsRuntimeService,
|
||
store_warmed_tray_close_mode,
|
||
)
|
||
|
||
store_warmed_tray_close_mode(TRAY_CLOSE_MODE_MINIMIZE_AND_CLOSE)
|
||
service = ProgramSettingsRuntimeService()
|
||
store_warmed_tray_close_mode(None)
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in click path"),
|
||
create=True,
|
||
):
|
||
self.assertEqual(service.peek_tray_close_mode(), TRAY_CLOSE_MODE_MINIMIZE_AND_CLOSE)
|
||
|
||
def test_startup_checks_are_delayed_after_ui_ready(self) -> None:
|
||
from main import post_startup_checks
|
||
from main.post_startup_checks import install_startup_checks
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callback = None
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callback = callback
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
self._callback(value)
|
||
|
||
signal = Signal()
|
||
startup_host = SimpleNamespace(
|
||
startup_interactive_ready=signal,
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
is_alive=Mock(return_value=True),
|
||
)
|
||
scheduled: list[tuple[int, object]] = []
|
||
queued_tasks: list[tuple[str, str]] = []
|
||
|
||
with (
|
||
patch.object(
|
||
post_startup_checks,
|
||
"schedule_after",
|
||
create=True,
|
||
side_effect=lambda delay_ms, callback: scheduled.append((delay_ms, callback)),
|
||
),
|
||
patch.object(
|
||
post_startup_checks,
|
||
"enqueue_subsystem_task",
|
||
side_effect=lambda queue, name, target: queued_tasks.append((queue, name)),
|
||
),
|
||
):
|
||
install_startup_checks(
|
||
startup_host,
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=Mock(),
|
||
)
|
||
signal.emit("ui_ready")
|
||
|
||
self.assertEqual(queued_tasks, [])
|
||
self.assertEqual(len(scheduled), 1)
|
||
self.assertGreaterEqual(scheduled[0][0], 6000)
|
||
|
||
scheduled[0][1]()
|
||
self.assertEqual(queued_tasks, [("checks", "StartupChecksWorker")])
|
||
|
||
def test_post_init_lists_and_telegram_are_delayed_past_dpi_start(self) -> None:
|
||
from main import post_startup_lists, post_startup_proxy
|
||
from main.post_startup_lists import install_lists_check
|
||
from main.post_startup_proxy import install_telegram_proxy_startup
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(value)
|
||
|
||
signal = Signal()
|
||
startup_host = SimpleNamespace(
|
||
startup_post_init_ready=signal,
|
||
startup_state=SimpleNamespace(post_init_ready=False),
|
||
is_alive=Mock(return_value=True),
|
||
)
|
||
scheduled: list[tuple[str, int, object]] = []
|
||
|
||
def schedule_lists(delay_ms, callback):
|
||
scheduled.append(("lists", delay_ms, callback))
|
||
|
||
def schedule_proxy(delay_ms, callback):
|
||
scheduled.append(("proxy", delay_ms, callback))
|
||
|
||
with (
|
||
patch.object(post_startup_lists, "schedule_after", side_effect=schedule_lists),
|
||
patch.object(post_startup_proxy, "schedule_after", side_effect=schedule_proxy),
|
||
patch.object(post_startup_lists, "enqueue_subsystem_task"),
|
||
patch.object(post_startup_proxy, "enqueue_subsystem_task"),
|
||
):
|
||
install_lists_check(
|
||
startup_host,
|
||
startup_lists_check=Mock(),
|
||
log_startup_metric=Mock(),
|
||
)
|
||
install_telegram_proxy_startup(
|
||
startup_host,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
log_startup_metric=Mock(),
|
||
)
|
||
signal.emit("post_init")
|
||
|
||
delays = {name: delay for name, delay, _callback in scheduled}
|
||
self.assertGreaterEqual(delays["proxy"], 6000)
|
||
self.assertGreaterEqual(delays["lists"], 7000)
|
||
|
||
def test_deferred_init_marks_ui_interactive_before_delayed_runtime_continue(self) -> None:
|
||
from PyQt6.QtCore import QTimer
|
||
import main.window_startup as window_startup
|
||
|
||
calls: list[str] = []
|
||
scheduled: list[tuple[int, object]] = []
|
||
|
||
class Signal:
|
||
def emit(self) -> None:
|
||
calls.append("continue_startup")
|
||
|
||
window = object.__new__(window_startup.WindowStartupMixin)
|
||
window.startup_state = SimpleNamespace(deferred_init_started=False)
|
||
window.continue_startup_requested = Signal()
|
||
window.build_ui = Mock(side_effect=lambda *_args: calls.append("build_ui"))
|
||
window.mark_startup_interactive = Mock(side_effect=lambda source: calls.append(f"interactive:{source}"))
|
||
|
||
with (
|
||
patch.object(
|
||
QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay_ms, callback: scheduled.append((delay_ms, callback)),
|
||
),
|
||
patch.object(window_startup, "emit_startup_metric") as metric,
|
||
):
|
||
window_startup.WindowStartupMixin._deferred_init(window)
|
||
|
||
self.assertEqual(
|
||
calls,
|
||
[
|
||
"build_ui",
|
||
"interactive:ui_ready",
|
||
],
|
||
)
|
||
self.assertEqual(len(scheduled), 1)
|
||
self.assertLessEqual(scheduled[0][0], 16)
|
||
metric.assert_called_once_with("StartupContinueAfterUiReadyQueued", f"{scheduled[0][0]}ms")
|
||
|
||
with patch.object(window_startup, "emit_startup_metric") as metric:
|
||
scheduled[0][1]()
|
||
self.assertEqual(calls[-1], "continue_startup")
|
||
metric.assert_called_once_with("StartupContinueAfterUiReadyDispatch", "continue_startup_requested")
|
||
window.mark_startup_interactive.assert_called_once_with("ui_ready")
|
||
|
||
def test_eager_page_creation_does_not_pump_qt_events_before_interactive(self) -> None:
|
||
import inspect
|
||
from app.page_names import PageName
|
||
from ui.page_host import WindowPageHost
|
||
import ui.page_host as page_host_module
|
||
|
||
calls: list[str] = []
|
||
|
||
class Page:
|
||
def objectName(self) -> str:
|
||
return "Page"
|
||
|
||
def setObjectName(self, _name: str) -> None:
|
||
pass
|
||
|
||
class Factory:
|
||
page_class_specs = {}
|
||
|
||
def create_page(self, page_name):
|
||
calls.append(f"create:{page_name.name}")
|
||
return SimpleNamespace(page=Page(), elapsed_ms=1)
|
||
|
||
host = WindowPageHost(SimpleNamespace(get_launch_method=lambda: "zapret2_mode"), Factory())
|
||
|
||
with (
|
||
patch.object(page_host_module, "apply_ui_language_to_page", side_effect=lambda *_args: None),
|
||
patch.object(page_host_module, "record_startup_page_init_metric", side_effect=lambda *_args: None),
|
||
patch.object(page_host_module, "log_page_timing", side_effect=lambda *_args, **_kwargs: None),
|
||
):
|
||
host.create_eager_pages((PageName.ZAPRET2_MODE_CONTROL,))
|
||
|
||
self.assertEqual(calls, ["create:ZAPRET2_MODE_CONTROL"])
|
||
source = inspect.getsource(WindowPageHost.create_eager_pages)
|
||
self.assertNotIn("pump_startup_ui", source)
|
||
|
||
def test_window_geometry_settings_are_read_once_for_startup_restore(self) -> None:
|
||
from settings import store as settings_store
|
||
|
||
data = {
|
||
"window": {
|
||
"x": 10,
|
||
"y": 20,
|
||
"width": 1200,
|
||
"height": 800,
|
||
"maximized": True,
|
||
}
|
||
}
|
||
|
||
# Геттер читает снимок настроек напрямую, минуя копию всего документа
|
||
# в read_settings(): геометрия запрашивается на каждом ресайзе окна.
|
||
with patch.object(
|
||
settings_store,
|
||
"_read_settings_cached_locked",
|
||
return_value=data,
|
||
) as read_cached_settings:
|
||
geometry = settings_store.get_window_geometry()
|
||
|
||
self.assertEqual(
|
||
geometry,
|
||
{
|
||
"x": 10,
|
||
"y": 20,
|
||
"width": 1200,
|
||
"height": 800,
|
||
"maximized": True,
|
||
},
|
||
)
|
||
read_cached_settings.assert_called_once_with()
|
||
|
||
def test_window_geometry_runtime_loads_saved_geometry_with_single_settings_read(self) -> None:
|
||
from settings import store as settings_store
|
||
from ui.window_geometry_runtime import SettingsWindowGeometryStore
|
||
|
||
data = {
|
||
"x": 10,
|
||
"y": 20,
|
||
"width": 1200,
|
||
"height": 800,
|
||
"maximized": True,
|
||
}
|
||
|
||
with patch.object(settings_store, "get_window_geometry", return_value=data) as get_geometry:
|
||
geometry = SettingsWindowGeometryStore().load()
|
||
|
||
self.assertEqual(geometry.position, (10, 20))
|
||
self.assertEqual(geometry.size, (1200, 800))
|
||
self.assertTrue(geometry.maximized)
|
||
get_geometry.assert_called_once_with()
|
||
|
||
def test_window_lifecycle_uses_canonical_minimum_size_for_restore_guard(self) -> None:
|
||
from config.window_metrics import MIN_HEIGHT, MIN_WIDTH
|
||
from main import window_lifecycle_setup
|
||
|
||
window = SimpleNamespace(
|
||
close_state=object(),
|
||
close_to_tray=Mock(),
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
)
|
||
window.setMinimumSize = Mock()
|
||
features = SimpleNamespace(
|
||
runtime=SimpleNamespace(snapshot=Mock()),
|
||
premium=object(),
|
||
telegram_proxy=object(),
|
||
tray=object(),
|
||
program_settings=SimpleNamespace(
|
||
tray_close_mode=Mock(return_value="normal"),
|
||
),
|
||
window_geometry=SimpleNamespace(create_geometry_save_worker=Mock()),
|
||
)
|
||
|
||
with (
|
||
patch.object(window_lifecycle_setup, "ApplicationLifecycle"),
|
||
patch.object(window_lifecycle_setup, "WindowCloseFlow"),
|
||
patch.object(window_lifecycle_setup, "build_application_lifecycle_window_port", return_value=object()),
|
||
patch.object(window_lifecycle_setup, "WindowGeometryRuntime") as geometry_runtime,
|
||
):
|
||
window_lifecycle_setup.attach_window_lifecycle(window, features)
|
||
|
||
window.setMinimumSize.assert_called_once_with(MIN_WIDTH, MIN_HEIGHT)
|
||
geometry_runtime.assert_called_once()
|
||
self.assertEqual(geometry_runtime.call_args.kwargs["min_width"], MIN_WIDTH)
|
||
self.assertEqual(geometry_runtime.call_args.kwargs["min_height"], MIN_HEIGHT)
|
||
|
||
def test_initial_ui_state_uses_single_settings_read_and_light_runtime_state_import(self) -> None:
|
||
import inspect
|
||
import app.initial_ui_state as initial_ui_state
|
||
from app.initial_ui_state import build_initial_ui_state
|
||
|
||
data = {
|
||
"appearance": {
|
||
"ui_language": "en",
|
||
"background_preset": "rkn_chan",
|
||
"mica_enabled": False,
|
||
"rkn_background": "rkn_tyan/rkn_background.jpg",
|
||
"animations_enabled": True,
|
||
"smooth_scroll_enabled": True,
|
||
"editor_smooth_scroll_enabled": True,
|
||
"garland_enabled": True,
|
||
"snowflakes_enabled": False,
|
||
},
|
||
"window": {
|
||
"opacity": 72,
|
||
},
|
||
"ui_state": {
|
||
"sidebar_expanded": False,
|
||
},
|
||
"program": {
|
||
"dpi_autostart": True,
|
||
"gui_autostart_enabled": False,
|
||
"strategy_launch_method": "zapret2_mode",
|
||
}
|
||
}
|
||
|
||
source = inspect.getsource(initial_ui_state)
|
||
self.assertIn("from winws_runtime.state import LaunchRuntimeService", source)
|
||
self.assertNotIn("from winws_runtime.public import LaunchRuntimeService", source)
|
||
|
||
import settings.appearance as appearance_settings
|
||
from ui.navigation.sidebar_state import clear_warmed_sidebar_expanded, peek_warmed_sidebar_expanded
|
||
|
||
def _clear_warmed_caches() -> None:
|
||
appearance_settings.clear_warmed_ui_language_cache()
|
||
appearance_settings.clear_warmed_rkn_background_cache()
|
||
appearance_settings.clear_warmed_background_preset_cache()
|
||
appearance_settings.clear_warmed_mica_enabled_cache()
|
||
appearance_settings.clear_warmed_window_opacity_cache()
|
||
appearance_settings.clear_warmed_animations_enabled_cache()
|
||
appearance_settings.clear_warmed_smooth_scroll_enabled_cache()
|
||
appearance_settings.clear_warmed_editor_smooth_scroll_enabled_cache()
|
||
appearance_settings.clear_warmed_premium_effects_cache()
|
||
clear_warmed_sidebar_expanded()
|
||
|
||
_clear_warmed_caches()
|
||
# Тест прогревает кэши значениями из data (ui_language="en" и т.д.);
|
||
# без сброса они утекают в последующие тесты всего прогона.
|
||
self.addCleanup(_clear_warmed_caches)
|
||
with patch("settings.store.read_settings", return_value=data) as read_settings:
|
||
state = build_initial_ui_state()
|
||
|
||
read_settings.assert_called_once_with()
|
||
self.assertEqual(state.launch_method, "zapret2_mode")
|
||
self.assertEqual(appearance_settings.peek_warmed_ui_language(), "en")
|
||
self.assertEqual(appearance_settings.peek_warmed_background_preset(), "rkn_chan")
|
||
self.assertEqual(appearance_settings.peek_warmed_mica_enabled(), False)
|
||
self.assertEqual(appearance_settings.peek_warmed_window_opacity(), 72)
|
||
self.assertEqual(appearance_settings.peek_warmed_rkn_background(), "rkn_tyan/rkn_background.jpg")
|
||
self.assertEqual(appearance_settings.peek_warmed_animations_enabled(), True)
|
||
self.assertEqual(appearance_settings.peek_warmed_smooth_scroll_enabled(), True)
|
||
self.assertEqual(appearance_settings.peek_warmed_editor_smooth_scroll_enabled(), True)
|
||
self.assertEqual(peek_warmed_sidebar_expanded(), False)
|
||
premium_effects = appearance_settings.peek_warmed_premium_effects()
|
||
self.assertIsNotNone(premium_effects)
|
||
self.assertEqual(premium_effects.garland_enabled, True)
|
||
self.assertEqual(premium_effects.snowflakes_enabled, False)
|
||
|
||
def test_control_pages_build_settings_sections_without_startup_delay(self) -> None:
|
||
from presets.ui.control.zapret1.page import Zapret1ModeControlPage
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
for page_cls in (Zapret1ModeControlPage, Zapret2ModeControlPage):
|
||
with self.subTest(page_cls=page_cls.__name__):
|
||
page_source = inspect.getsource(page_cls)
|
||
build_ui_source = inspect.getsource(page_cls._build_ui)
|
||
|
||
self.assertIn("_build_settings_sections", build_ui_source)
|
||
self.assertIn("_attach_program_settings_runtime", build_ui_source)
|
||
self.assertIn("_schedule_additional_settings_reload(force=True)", build_ui_source)
|
||
self.assertNotIn("startup_post_init_ready", page_source)
|
||
self.assertNotIn("startup_interactive_ready_for_deferred_sections", page_source)
|
||
self.assertNotIn("_deferred_sections_hydrated", page_source)
|
||
self.assertNotIn("_deferred_sections_built", page_source)
|
||
|
||
def test_zapret2_control_initial_state_is_not_delayed_by_startup_gate(self) -> None:
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
bind_source = inspect.getsource(Zapret2ModeControlPage.bind_ui_state_store)
|
||
|
||
self.assertNotIn("defer_initial_state", bind_source)
|
||
self.assertNotIn("_wait_for_startup_interactive_before_initial_ui_state", bind_source)
|
||
self.assertIn("bind_control_ui_state_store", bind_source)
|
||
|
||
def test_zapret2_control_loads_top_summary_through_worker_without_startup_delay(self) -> None:
|
||
from app.state_store import AppUiState
|
||
from presets.ui.control.additional_settings_runtime import create_refresh_runtime
|
||
from presets.ui.control.zapret2 import page as zapret2_page
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
class WorkerSignal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, *args) -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(*args)
|
||
|
||
class FakeTopSummaryWorker:
|
||
def __init__(
|
||
self,
|
||
request_id: int,
|
||
get_selected_source_preset_display,
|
||
get_enabled_profile_count_snapshot,
|
||
) -> None:
|
||
self._request_id = int(request_id)
|
||
self._get_selected_source_preset_display = get_selected_source_preset_display
|
||
self._get_enabled_profile_count_snapshot = get_enabled_profile_count_snapshot
|
||
self.loaded = WorkerSignal()
|
||
self.failed = WorkerSignal()
|
||
self.finished = WorkerSignal()
|
||
|
||
def isRunning(self) -> bool:
|
||
return False
|
||
|
||
def start(self) -> None:
|
||
preset_text, preset_tooltip = self._get_selected_source_preset_display("zapret2_mode")
|
||
profile_count = self._get_enabled_profile_count_snapshot("zapret2_mode")
|
||
self.loaded.emit(
|
||
self._request_id,
|
||
SimpleNamespace(
|
||
preset_text=preset_text,
|
||
preset_tooltip=preset_tooltip,
|
||
profile_count=profile_count,
|
||
),
|
||
)
|
||
self.finished.emit()
|
||
|
||
def deleteLater(self) -> None:
|
||
pass
|
||
|
||
control_page = Zapret2ModeControlPage.__new__(Zapret2ModeControlPage)
|
||
control_page._cleanup_in_progress = False
|
||
control_page._refresh_runtime = create_refresh_runtime()
|
||
control_page._refresh_runtime.additional_settings_dirty = False
|
||
control_page._ui_language = "ru"
|
||
control_page._ui_state_store = None
|
||
control_page._top_summary_profile_retry_count = 0
|
||
control_page._top_summary_profile_retry_pending = False
|
||
control_page.top_summary = SimpleNamespace(
|
||
set_preset=Mock(),
|
||
set_profile_count=Mock(),
|
||
set_premium=Mock(),
|
||
)
|
||
control_page._get_selected_source_preset_display = Mock(return_value=("Preset", "Preset"))
|
||
control_page._get_enabled_profile_count_snapshot = Mock(return_value=2)
|
||
control_page._create_top_summary_worker = (
|
||
lambda request_id, **_kwargs: FakeTopSummaryWorker(
|
||
request_id,
|
||
control_page._get_selected_source_preset_display,
|
||
control_page._get_enabled_profile_count_snapshot,
|
||
)
|
||
)
|
||
control_page.window = Mock(return_value=SimpleNamespace(startup_state=SimpleNamespace(interactive_logged=False)))
|
||
control_page.set_loading = Mock()
|
||
control_page.update_status = Mock()
|
||
control_page.update_strategy = Mock()
|
||
control_page._refresh_last_status_message = Mock()
|
||
|
||
Zapret2ModeControlPage._on_ui_state_changed(control_page, AppUiState(), frozenset())
|
||
|
||
control_page._get_selected_source_preset_display.assert_called_once()
|
||
control_page._get_enabled_profile_count_snapshot.assert_called_once()
|
||
control_page.top_summary.set_preset.assert_called_with("Preset")
|
||
control_page.top_summary.set_profile_count.assert_called_with(2)
|
||
|
||
def test_zapret2_control_retries_top_summary_profile_count_after_warmup(self) -> None:
|
||
from app.state_store import AppUiState
|
||
from presets.ui.control.additional_settings_runtime import create_refresh_runtime
|
||
from presets.ui.control.zapret2 import page as zapret2_page
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
scheduled: list[tuple[int, object]] = []
|
||
profile_count = Mock(side_effect=[None, 4])
|
||
|
||
class WorkerSignal:
|
||
def __init__(self) -> None:
|
||
self._callbacks: list[object] = []
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callbacks.append(callback)
|
||
|
||
def emit(self, *args) -> None:
|
||
for callback in list(self._callbacks):
|
||
callback(*args)
|
||
|
||
class FakeTopSummaryWorker:
|
||
def __init__(
|
||
self,
|
||
request_id: int,
|
||
get_selected_source_preset_display,
|
||
get_enabled_profile_count_snapshot,
|
||
) -> None:
|
||
self._request_id = int(request_id)
|
||
self._get_selected_source_preset_display = get_selected_source_preset_display
|
||
self._get_enabled_profile_count_snapshot = get_enabled_profile_count_snapshot
|
||
self.loaded = WorkerSignal()
|
||
self.failed = WorkerSignal()
|
||
self.finished = WorkerSignal()
|
||
|
||
def isRunning(self) -> bool:
|
||
return False
|
||
|
||
def start(self) -> None:
|
||
preset_text, preset_tooltip = self._get_selected_source_preset_display("zapret2_mode")
|
||
current_profile_count = self._get_enabled_profile_count_snapshot("zapret2_mode")
|
||
self.loaded.emit(
|
||
self._request_id,
|
||
SimpleNamespace(
|
||
preset_text=preset_text,
|
||
preset_tooltip=preset_tooltip,
|
||
profile_count=current_profile_count,
|
||
),
|
||
)
|
||
self.finished.emit()
|
||
|
||
def deleteLater(self) -> None:
|
||
pass
|
||
|
||
control_page = Zapret2ModeControlPage.__new__(Zapret2ModeControlPage)
|
||
control_page._cleanup_in_progress = False
|
||
control_page._ui_language = "ru"
|
||
control_page._ui_state_store = None
|
||
control_page._refresh_runtime = create_refresh_runtime()
|
||
control_page._top_summary_profile_retry_count = 0
|
||
control_page._top_summary_profile_retry_pending = False
|
||
control_page.top_summary = SimpleNamespace(
|
||
set_preset=Mock(),
|
||
set_profile_count=Mock(),
|
||
set_premium=Mock(),
|
||
)
|
||
control_page._get_selected_source_preset_display = Mock(return_value=("Preset", "Preset"))
|
||
control_page._get_enabled_profile_count_snapshot = profile_count
|
||
control_page._create_top_summary_worker = (
|
||
lambda request_id, **_kwargs: FakeTopSummaryWorker(
|
||
request_id,
|
||
control_page._get_selected_source_preset_display,
|
||
control_page._get_enabled_profile_count_snapshot,
|
||
)
|
||
)
|
||
|
||
with patch.object(
|
||
zapret2_page.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda delay_ms, callback: scheduled.append((delay_ms, callback)),
|
||
):
|
||
Zapret2ModeControlPage._refresh_top_summary(control_page, AppUiState())
|
||
|
||
control_page.top_summary.set_profile_count.assert_called_with(None)
|
||
self.assertEqual(len(scheduled), 1)
|
||
self.assertGreaterEqual(scheduled[0][0], zapret2_page.TOP_SUMMARY_PROFILE_RETRY_MS)
|
||
|
||
scheduled[0][1]()
|
||
|
||
control_page.top_summary.set_profile_count.assert_called_with(4)
|
||
self.assertEqual(control_page._top_summary_profile_retry_count, 0)
|
||
self.assertFalse(control_page._top_summary_profile_retry_pending)
|
||
|
||
def test_control_pages_refresh_top_summary_when_profile_summary_changes(self) -> None:
|
||
from app.state_store import AppUiState
|
||
from presets.ui.control.zapret1.page import Zapret1ModeControlPage
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
for page_cls in (Zapret2ModeControlPage, Zapret1ModeControlPage):
|
||
with self.subTest(page_cls=page_cls.__name__):
|
||
control_page = page_cls.__new__(page_cls)
|
||
control_page._cleanup_in_progress = False
|
||
control_page._refresh_runtime = SimpleNamespace(additional_settings_dirty=False)
|
||
control_page._startup_can_refresh_top_summary = Mock(return_value=True)
|
||
control_page._refresh_top_summary = Mock()
|
||
control_page._wait_for_startup_interactive_before_top_summary = Mock()
|
||
control_page.set_loading = Mock()
|
||
control_page.update_status = Mock()
|
||
control_page.update_strategy = Mock()
|
||
control_page._refresh_last_status_message = Mock()
|
||
|
||
page_cls._on_ui_state_changed(
|
||
control_page,
|
||
AppUiState(current_strategy_summary="48 включено"),
|
||
frozenset({"current_strategy_summary"}),
|
||
)
|
||
|
||
control_page._refresh_top_summary.assert_called_once()
|
||
|
||
def test_control_pages_do_not_reload_top_summary_for_active_preset_marker_only(self) -> None:
|
||
from app.state_store import AppUiState
|
||
from presets.ui.control.zapret1.page import Zapret1ModeControlPage
|
||
from presets.ui.control.zapret2.page import Zapret2ModeControlPage
|
||
|
||
for page_cls in (Zapret2ModeControlPage, Zapret1ModeControlPage):
|
||
with self.subTest(page_cls=page_cls.__name__):
|
||
control_page = page_cls.__new__(page_cls)
|
||
control_page._cleanup_in_progress = False
|
||
control_page._refresh_runtime = SimpleNamespace(additional_settings_dirty=False)
|
||
control_page._startup_can_refresh_top_summary = Mock(return_value=True)
|
||
control_page._refresh_top_summary = Mock()
|
||
control_page._wait_for_startup_interactive_before_top_summary = Mock()
|
||
control_page._apply_selected_preset_name_fast = Mock()
|
||
control_page._refresh_preset_name = Mock()
|
||
control_page._schedule_additional_settings_reload = Mock()
|
||
control_page._schedule_additional_settings_reload_after_preset_switch = Mock()
|
||
control_page._apply_pending_mode_refresh_if_ready = Mock()
|
||
control_page._apply_pending_preset_name_refresh = Mock()
|
||
control_page._apply_pending_additional_settings_refresh = Mock()
|
||
control_page.run_when_page_ready = Mock()
|
||
control_page.isVisible = Mock(return_value=True)
|
||
control_page.set_loading = Mock()
|
||
control_page.update_status = Mock()
|
||
control_page.update_strategy = Mock()
|
||
control_page._refresh_last_status_message = Mock()
|
||
|
||
page_cls._on_ui_state_changed(
|
||
control_page,
|
||
AppUiState(current_strategy_summary="Профили"),
|
||
frozenset({"active_preset_revision"}),
|
||
)
|
||
|
||
control_page._refresh_top_summary.assert_not_called()
|
||
self.assertTrue(control_page._refresh_runtime.additional_settings_dirty)
|
||
control_page._schedule_additional_settings_reload.assert_not_called()
|
||
control_page._schedule_additional_settings_reload_after_preset_switch.assert_called_once_with()
|
||
|
||
def test_window_page_actions_route_pages_through_adapter(self) -> None:
|
||
from app.page_names import PageName
|
||
from main import window_page_actions
|
||
from main.window_page_actions import build_window_page_actions
|
||
|
||
window = SimpleNamespace(
|
||
set_status=Mock(),
|
||
window_notification_center=SimpleNamespace(notify=Mock()),
|
||
request_exit=Mock(),
|
||
open_connection_test=Mock(),
|
||
open_folder=Mock(),
|
||
)
|
||
appearance_actions = SimpleNamespace(
|
||
set_garland_enabled=Mock(),
|
||
set_snowflakes_enabled=Mock(),
|
||
set_window_opacity=Mock(),
|
||
)
|
||
|
||
with patch.object(window_page_actions, "show_page", return_value=True) as routed_show_page:
|
||
actions = build_window_page_actions(
|
||
window=window,
|
||
appearance_actions=appearance_actions,
|
||
)
|
||
self.assertTrue(actions.show_page(PageName.ABOUT, allow_internal=True))
|
||
|
||
routed_show_page.assert_called_once_with(window, PageName.ABOUT, allow_internal=True)
|
||
|
||
def test_profile_setup_change_invalidates_loaded_order_page_without_ensuring_it(self) -> None:
|
||
from app.page_names import PageName
|
||
from main import window_page_presenters
|
||
|
||
calls: list[tuple[PageName, str, dict, bool]] = []
|
||
|
||
def send_page_command(_window, page_name, command, payload, *, ensure=True):
|
||
calls.append((page_name, command, payload, ensure))
|
||
return True
|
||
|
||
with patch.object(window_page_presenters, "send_page_command", side_effect=send_page_command):
|
||
self.assertTrue(
|
||
window_page_presenters.apply_profile_setup_change_for_method(
|
||
object(),
|
||
"zapret2_mode",
|
||
"profile:1",
|
||
"strategy",
|
||
)
|
||
)
|
||
|
||
self.assertEqual(calls[0][0], PageName.ZAPRET2_PRESET_SETUP)
|
||
self.assertEqual(calls[0][1], "profile_setup_changed")
|
||
self.assertFalse(calls[0][3])
|
||
self.assertEqual(calls[1][0], PageName.ZAPRET2_PROFILE_ORDER)
|
||
self.assertEqual(calls[1][1], "profile_order_changed")
|
||
self.assertFalse(calls[1][3])
|
||
|
||
def test_window_notifications_route_pages_through_adapter(self) -> None:
|
||
from app.page_names import PageName
|
||
from main import window_notifications_setup
|
||
from main.window_notifications_setup import attach_window_notifications
|
||
|
||
captured: dict[str, object] = {}
|
||
|
||
class NotificationCenter:
|
||
def __init__(self, *_args, show_page, **_kwargs) -> None:
|
||
captured["show_page"] = show_page
|
||
self.notify = Mock()
|
||
|
||
def register_global_error_notifier(self) -> None:
|
||
pass
|
||
|
||
window = SimpleNamespace(
|
||
startup_state=object(),
|
||
isVisible=Mock(return_value=True),
|
||
isMinimized=Mock(return_value=False),
|
||
log_startup_metric=Mock(),
|
||
)
|
||
features = SimpleNamespace(
|
||
runtime=SimpleNamespace(
|
||
configure_notifications=Mock(),
|
||
is_available=Mock(return_value=True),
|
||
cancel_start_after_conflict_prompt=Mock(),
|
||
execute_windivert_autofix=Mock(),
|
||
install_windows_server_wlanapi=Mock(return_value=(True, "")),
|
||
prepare_launch_conflict_resolution=Mock(),
|
||
continue_start_after_conflict_resolution=Mock(),
|
||
),
|
||
tray=SimpleNamespace(
|
||
show_notification_if_available=Mock(),
|
||
configure=Mock(),
|
||
),
|
||
)
|
||
|
||
with (
|
||
patch.object(window_notifications_setup, "WindowNotificationCenter", NotificationCenter),
|
||
patch.object(window_notifications_setup, "show_page", return_value=True) as routed_show_page,
|
||
):
|
||
attach_window_notifications(window, features)
|
||
self.assertTrue(captured["show_page"](PageName.SERVERS, allow_internal=True))
|
||
|
||
routed_show_page.assert_called_once_with(window, PageName.SERVERS, allow_internal=True)
|
||
|
||
def test_control_start_waits_until_runtime_is_available(self) -> None:
|
||
from presets.ui.control import control_page_shared
|
||
from presets.ui.control.control_page_shared import ControlPageActionMixin
|
||
|
||
class Page(ControlPageActionMixin):
|
||
def __init__(self) -> None:
|
||
self.loading_calls: list[tuple[bool, str]] = []
|
||
self.status_calls: list[str] = []
|
||
self._runtime_actions = SimpleNamespace(
|
||
is_available=Mock(side_effect=[False, False, True]),
|
||
start=Mock(return_value=True),
|
||
)
|
||
self._set_status_callback = self.status_calls.append
|
||
|
||
def set_loading(self, loading: bool, text: str = "") -> None:
|
||
self.loading_calls.append((loading, text))
|
||
|
||
page = Page()
|
||
scheduled: list[object] = []
|
||
|
||
with patch.object(
|
||
control_page_shared.QTimer,
|
||
"singleShot",
|
||
side_effect=lambda _delay_ms, callback: scheduled.append(callback),
|
||
):
|
||
page._start_dpi()
|
||
self.assertEqual(len(scheduled), 1)
|
||
scheduled.pop(0)()
|
||
self.assertEqual(len(scheduled), 1)
|
||
scheduled.pop(0)()
|
||
|
||
page._runtime_actions.start.assert_called_once_with()
|
||
self.assertIn((True, "Подготовка запуска..."), page.loading_calls)
|
||
self.assertEqual(page.loading_calls[-1], (False, ""))
|
||
self.assertIn("Подготовка запуска...", page.status_calls)
|
||
|
||
def test_post_startup_tasks_install_hosts_page_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
hosts_feature = object()
|
||
profile_feature = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
hosts_feature=hosts_feature,
|
||
profile_feature=profile_feature,
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup") as install_hosts_page_warmup,
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_hosts_page_warmup.assert_called_once_with(
|
||
startup_host,
|
||
hosts_feature=hosts_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
)
|
||
self.assertFalse(hasattr(post_startup, "install_page_warmup"))
|
||
|
||
def test_post_startup_tasks_install_telegram_proxy_page_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=object(),
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup") as install_telegram_proxy_page_warmup,
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
patch.object(post_startup, "install_startup_audit"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_telegram_proxy_page_warmup.assert_called_once_with(
|
||
startup_host,
|
||
log_startup_metric=log_startup_metric,
|
||
)
|
||
|
||
def test_post_startup_tasks_install_profile_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
profile_feature = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=profile_feature,
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup") as install_profile_warmup,
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_profile_warmup.assert_called_once_with(
|
||
startup_host,
|
||
profile_feature=profile_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
current_launch_method="",
|
||
on_profile_warmup_ready=None,
|
||
)
|
||
|
||
def test_post_startup_profile_warmup_refreshes_profile_summary_after_ready(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
profile_feature = object()
|
||
presets_feature = SimpleNamespace(refresh_profile_strategy_summary_in_store=Mock())
|
||
ui_state_store = object()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=profile_feature,
|
||
presets_feature=presets_feature,
|
||
ui_state_store=ui_state_store,
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=Mock(),
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup") as install_profile_warmup,
|
||
patch.object(post_startup, "install_user_presets_warmup"),
|
||
patch.object(post_startup, "install_remote_presets_sync"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
callback = install_profile_warmup.call_args.kwargs["on_profile_warmup_ready"]
|
||
self.assertIsNotNone(callback)
|
||
|
||
callback("zapret2_mode")
|
||
|
||
presets_feature.refresh_profile_strategy_summary_in_store.assert_called_once_with(
|
||
method="zapret2_mode",
|
||
profile_feature=profile_feature,
|
||
ui_state_store=ui_state_store,
|
||
)
|
||
|
||
def test_post_startup_tasks_install_user_presets_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
presets_feature = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=object(),
|
||
presets_feature=presets_feature,
|
||
dns_feature=object(),
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_user_presets_warmup") as install_user_presets_warmup,
|
||
patch.object(post_startup, "install_remote_presets_sync") as install_remote_presets_sync,
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_user_presets_warmup.assert_called_once_with(
|
||
startup_host,
|
||
presets_feature=presets_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
current_launch_method="",
|
||
)
|
||
install_remote_presets_sync.assert_called_once_with(
|
||
startup_host,
|
||
presets_feature=presets_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
notify=deps.notify,
|
||
)
|
||
|
||
def test_user_presets_runtime_uses_cached_metadata_before_worker(self) -> None:
|
||
from presets.user_presets_runtime_service import UserPresetsRuntimeAdapter, UserPresetsRuntimeService
|
||
|
||
service = UserPresetsRuntimeService(scope_key="winws2")
|
||
page = SimpleNamespace(
|
||
isVisible=Mock(return_value=True),
|
||
)
|
||
service.update_cached_folder_state({})
|
||
service._request_rows_plan_refresh = Mock()
|
||
adapter = UserPresetsRuntimeAdapter(
|
||
bulk_reset_running=Mock(return_value=False),
|
||
read_single_metadata=Mock(return_value=None),
|
||
selected_source_file_name=Mock(return_value="a.txt"),
|
||
presets_dir=Mock(),
|
||
cached_metadata=Mock(return_value={"a.txt": {"display_name": "A"}}),
|
||
load_all_metadata=Mock(return_value={}),
|
||
load_folder_state=Mock(return_value={}),
|
||
build_rows_plan=Mock(),
|
||
apply_rows_plan=Mock(),
|
||
)
|
||
service.attach_page(page, adapter)
|
||
|
||
service.refresh_presets_view_if_possible(page)
|
||
|
||
service._request_rows_plan_refresh.assert_called_once_with(
|
||
{"a.txt": {"display_name": "A"}},
|
||
{},
|
||
None,
|
||
page,
|
||
)
|
||
adapter.load_all_metadata.assert_not_called()
|
||
|
||
def test_post_startup_tasks_install_dns_page_data_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
dns_feature = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=object(),
|
||
dns_feature=dns_feature,
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup") as install_dns_page_data_warmup,
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_dns_page_data_warmup.assert_called_once_with(
|
||
startup_host,
|
||
dns_feature=dns_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
)
|
||
|
||
def test_post_startup_tasks_install_backend_page_data_warmup(self) -> None:
|
||
from main import post_startup
|
||
from main.post_startup import PostStartupDeps, install_post_startup_tasks
|
||
|
||
startup_host = object()
|
||
premium_feature = object()
|
||
logs_feature = object()
|
||
log_startup_metric = Mock()
|
||
deps = PostStartupDeps(
|
||
startup_host=startup_host,
|
||
profile_feature=object(),
|
||
dns_feature=object(),
|
||
premium_feature=premium_feature,
|
||
logs_feature=logs_feature,
|
||
notify=Mock(),
|
||
notify_many=Mock(),
|
||
set_status=Mock(),
|
||
log_startup_metric=log_startup_metric,
|
||
start_proxy_if_enabled_async=Mock(),
|
||
startup_lists_check=Mock(),
|
||
apply_dns_on_startup_async=Mock(),
|
||
install_tray_post_startup=Mock(),
|
||
updater_feature=Mock(),
|
||
)
|
||
|
||
with (
|
||
patch.object(post_startup, "install_startup_checks"),
|
||
patch.object(post_startup, "install_deferred_maintenance"),
|
||
patch.object(post_startup, "install_telegram_proxy_startup"),
|
||
patch.object(post_startup, "install_telegram_proxy_page_warmup"),
|
||
patch.object(post_startup, "install_secondary_page_warmup"),
|
||
patch.object(post_startup, "install_lists_check"),
|
||
patch.object(post_startup, "install_dns_startup"),
|
||
patch.object(post_startup, "install_dns_page_data_warmup"),
|
||
patch.object(post_startup, "install_hosts_page_warmup"),
|
||
patch.object(post_startup, "install_backend_page_data_warmup") as install_backend_page_data_warmup,
|
||
patch.object(post_startup, "install_profile_warmup"),
|
||
patch.object(post_startup, "install_update_check"),
|
||
patch.object(post_startup, "install_cpu_diagnostic"),
|
||
patch.object(post_startup, "install_qt_event_diagnostic_probe"),
|
||
patch.object(post_startup, "install_global_exception_handler"),
|
||
):
|
||
install_post_startup_tasks(deps)
|
||
|
||
install_backend_page_data_warmup.assert_called_once_with(
|
||
startup_host,
|
||
premium_feature=premium_feature,
|
||
logs_feature=logs_feature,
|
||
log_startup_metric=log_startup_metric,
|
||
)
|
||
|
||
def test_dns_page_data_warmup_uses_backend_cache_without_page_host(self) -> None:
|
||
from main import post_startup_dns_warmup
|
||
from main.post_startup_dns_warmup import install_dns_page_data_warmup
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callback = None
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callback = callback
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
self._callback(value)
|
||
|
||
signal = Signal()
|
||
startup_host = SimpleNamespace(
|
||
startup_interactive_ready=signal,
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
is_alive=Mock(return_value=True),
|
||
)
|
||
dns_feature = SimpleNamespace(warm_page_data_cache=Mock(return_value=object()))
|
||
metric = Mock()
|
||
delays: list[int] = []
|
||
queued_tasks: list[tuple[str, str]] = []
|
||
|
||
with (
|
||
patch.object(
|
||
post_startup_dns_warmup,
|
||
"schedule_after",
|
||
side_effect=lambda delay_ms, callback: delays.append(delay_ms) or callback(),
|
||
),
|
||
patch.object(
|
||
post_startup_dns_warmup,
|
||
"enqueue_subsystem_task",
|
||
side_effect=lambda queue, name, target: queued_tasks.append((queue, name)) or target(),
|
||
),
|
||
):
|
||
install_dns_page_data_warmup(
|
||
startup_host,
|
||
dns_feature=dns_feature,
|
||
log_startup_metric=metric,
|
||
)
|
||
signal.emit("interactive")
|
||
|
||
self.assertEqual(delays, [10000])
|
||
self.assertEqual(queued_tasks, [("dns", "DnsPageDataWarmup")])
|
||
dns_feature.warm_page_data_cache.assert_called_once_with()
|
||
self.assertFalse(hasattr(startup_host, "warm_page"))
|
||
metric.assert_any_call("StartupNetworkDataWarmupQueued", "10000ms after interactive")
|
||
metric.assert_any_call("StartupPostInitNetworkDataWarmupStarted", "backend_cache")
|
||
|
||
def test_profile_warmup_targets_only_active_method(self) -> None:
|
||
from main.post_startup_profile_warmup import profile_warmup_method
|
||
|
||
self.assertEqual(profile_warmup_method("zapret1_mode"), "zapret1_mode")
|
||
self.assertEqual(profile_warmup_method("zapret2_mode"), "zapret2_mode")
|
||
self.assertEqual(profile_warmup_method("orchestra"), "zapret2_mode")
|
||
|
||
def test_profile_warmup_warms_only_current_method_after_interactive_ready(self) -> None:
|
||
from app.page_names import PageName
|
||
from main import post_startup_profile_warmup
|
||
from main.post_startup_profile_warmup import install_profile_warmup
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callback = None
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callback = callback
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
self._callback(value)
|
||
|
||
signal = Signal()
|
||
warmed_preset_setup_page = SimpleNamespace(warmup_initial_load=Mock(return_value=True))
|
||
startup_host = SimpleNamespace(
|
||
startup_interactive_ready=signal,
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
is_alive=Mock(return_value=True),
|
||
ensure_page=Mock(return_value=warmed_preset_setup_page),
|
||
show_page=Mock(),
|
||
)
|
||
profile_feature = SimpleNamespace(warm_profile_list=Mock(return_value=object()))
|
||
metric = Mock()
|
||
delays: list[int] = []
|
||
queued_tasks: list[tuple[str, str]] = []
|
||
ready_methods: list[str] = []
|
||
|
||
with (
|
||
patch.object(
|
||
post_startup_profile_warmup,
|
||
"schedule_after",
|
||
side_effect=lambda delay_ms, callback: delays.append(delay_ms) or callback(),
|
||
),
|
||
patch.object(
|
||
post_startup_profile_warmup,
|
||
"enqueue_subsystem_task",
|
||
side_effect=lambda queue, name, target: queued_tasks.append((queue, name)) or target(),
|
||
),
|
||
):
|
||
install_profile_warmup(
|
||
startup_host,
|
||
profile_feature=profile_feature,
|
||
log_startup_metric=metric,
|
||
current_launch_method="zapret1_mode",
|
||
on_profile_warmup_ready=ready_methods.append,
|
||
)
|
||
signal.emit("interactive")
|
||
|
||
self.assertEqual(delays, [0, 1000, 2200])
|
||
self.assertEqual(
|
||
queued_tasks,
|
||
[("profile", "ProfileWarmup-zapret1_mode")],
|
||
)
|
||
self.assertEqual(ready_methods, ["zapret1_mode"])
|
||
self.assertEqual(
|
||
[recorded_call.args[0] for recorded_call in profile_feature.warm_profile_list.call_args_list],
|
||
["zapret1_mode"],
|
||
)
|
||
metric.assert_any_call(
|
||
"StartupProfileWarmupQueued",
|
||
"0ms current after interactive",
|
||
)
|
||
metric.assert_any_call("StartupProfileWarmupStarted", "zapret1_mode")
|
||
startup_host.ensure_page.assert_has_calls(
|
||
[
|
||
call(PageName.ZAPRET1_PROFILE_SETUP),
|
||
call(PageName.ZAPRET1_PRESET_SETUP),
|
||
]
|
||
)
|
||
warmed_preset_setup_page.warmup_initial_load.assert_called_once_with()
|
||
startup_host.show_page.assert_not_called()
|
||
metric.assert_any_call("StartupProfileSetupUiWarmupQueued", "1000ms after interactive")
|
||
metric.assert_any_call("StartupPresetSetupUiWarmupQueued", "2200ms after interactive")
|
||
metric.assert_any_call("StartupProfileSetupUiWarmupFinished", "ZAPRET1_PROFILE_SETUP")
|
||
metric.assert_any_call("StartupPresetSetupUiWarmupFinished", "ZAPRET1_PRESET_SETUP")
|
||
|
||
def test_user_presets_warmup_staggers_current_and_secondary_methods_after_interactive_ready(self) -> None:
|
||
from main import post_startup_user_presets_warmup
|
||
from main.post_startup_user_presets_warmup import install_user_presets_warmup
|
||
|
||
class Signal:
|
||
def __init__(self) -> None:
|
||
self._callback = None
|
||
|
||
def connect(self, callback) -> None:
|
||
self._callback = callback
|
||
|
||
def emit(self, value: str = "") -> None:
|
||
self._callback(value)
|
||
|
||
signal = Signal()
|
||
startup_host = SimpleNamespace(
|
||
startup_interactive_ready=signal,
|
||
startup_state=SimpleNamespace(interactive_logged=False),
|
||
is_alive=Mock(return_value=True),
|
||
)
|
||
presets_feature = SimpleNamespace(warm_preset_list_metadata_cache=Mock(return_value={"a.txt": {}}))
|
||
metric = Mock()
|
||
delays: list[int] = []
|
||
queued_tasks: list[tuple[str, str]] = []
|
||
|
||
with (
|
||
patch.object(
|
||
post_startup_user_presets_warmup,
|
||
"schedule_after",
|
||
side_effect=lambda delay_ms, callback: delays.append(delay_ms) or callback(),
|
||
),
|
||
patch.object(
|
||
post_startup_user_presets_warmup,
|
||
"enqueue_subsystem_task",
|
||
side_effect=lambda queue, name, target: queued_tasks.append((queue, name)) or target(),
|
||
),
|
||
):
|
||
install_user_presets_warmup(
|
||
startup_host,
|
||
presets_feature=presets_feature,
|
||
log_startup_metric=metric,
|
||
current_launch_method="zapret1_mode",
|
||
)
|
||
signal.emit("interactive")
|
||
|
||
self.assertEqual(delays, [8000, 15000])
|
||
self.assertEqual(
|
||
queued_tasks,
|
||
[("presets", "UserPresetsWarmup-zapret1_mode"), ("presets", "UserPresetsWarmup-zapret2_mode")],
|
||
)
|
||
self.assertEqual(
|
||
[recorded_call.args[0] for recorded_call in presets_feature.warm_preset_list_metadata_cache.call_args_list],
|
||
["zapret1_mode", "zapret2_mode"],
|
||
)
|
||
metric.assert_any_call(
|
||
"StartupUserPresetsWarmupQueued",
|
||
"8000ms current after interactive; 15000ms secondary after interactive",
|
||
)
|
||
metric.assert_any_call("StartupUserPresetsWarmupStarted", "zapret1_mode")
|
||
metric.assert_any_call("StartupUserPresetsWarmupStarted", "zapret2_mode")
|
||
|
||
def test_win11_radio_option_recommended_badge_does_not_require_global_flag(self) -> None:
|
||
import ui.widgets.win11_controls as win11_controls
|
||
|
||
class Badge:
|
||
def __init__(self, *_args, **_kwargs) -> None:
|
||
pass
|
||
|
||
self.assertTrue(hasattr(win11_controls, "_HAS_INFO_BADGE"))
|
||
|
||
with patch.object(win11_controls, "_HAS_INFO_BADGE", True):
|
||
self.assertTrue(win11_controls._should_use_info_badge(Badge, object()))
|
||
|
||
with patch.object(win11_controls, "_HAS_INFO_BADGE", False):
|
||
self.assertFalse(win11_controls._should_use_info_badge(Badge, object()))
|
||
|
||
def test_hide_window_hides_without_changing_window_state(self) -> None:
|
||
from PyQt6.QtCore import Qt
|
||
from ui.window_adapter import hide_window
|
||
|
||
calls: list[str] = []
|
||
|
||
class Window:
|
||
def windowState(self):
|
||
return Qt.WindowState.WindowMinimized | Qt.WindowState.WindowMaximized
|
||
|
||
def setWindowState(self, state) -> None:
|
||
calls.append(f"state:{int(state.value)}")
|
||
self.state = state
|
||
|
||
def hide(self) -> None:
|
||
calls.append("hide")
|
||
|
||
window = Window()
|
||
|
||
hide_window(window)
|
||
|
||
self.assertEqual(calls, ["hide"])
|
||
self.assertFalse(hasattr(window, "state"))
|
||
|
||
def test_startup_runtime_keeps_coordinator_alive_for_queued_phase_two(self) -> None:
|
||
from main import startup_coordinator
|
||
from main.window_startup_setup import attach_startup_deps_to_window
|
||
|
||
signal = SimpleNamespace(emit=Mock())
|
||
window = SimpleNamespace(
|
||
start_in_tray=False,
|
||
set_status=Mock(),
|
||
mark_startup_core_ready=Mock(),
|
||
mark_startup_post_init_done=Mock(),
|
||
log_startup_metric=Mock(),
|
||
finalize_ui_bootstrap_requested=signal,
|
||
)
|
||
features = SimpleNamespace(
|
||
premium=SimpleNamespace(prepare_subscription=Mock()),
|
||
program_settings=SimpleNamespace(ensure_gui_autostart_migrated=Mock(return_value=False)),
|
||
runtime=SimpleNamespace(
|
||
init_launch_runtime_api=Mock(),
|
||
init_launch_runtime=Mock(),
|
||
init_process_monitor=Mock(),
|
||
init_core_startup=Mock(),
|
||
),
|
||
tray=SimpleNamespace(
|
||
init=Mock(),
|
||
is_initialized=Mock(return_value=False),
|
||
),
|
||
)
|
||
|
||
runtime = attach_startup_deps_to_window(window, features)
|
||
|
||
with patch.object(startup_coordinator, "run_queued", side_effect=lambda _callback: None):
|
||
runtime.continue_deferred_init()
|
||
|
||
self.assertIsNotNone(runtime.startup_coordinator)
|
||
self.assertIsInstance(runtime.startup_coordinator, startup_coordinator.StartupCoordinator)
|
||
signal.emit.assert_called_once()
|
||
|
||
|
||
class EarlyStartupCrashTests(unittest.TestCase):
|
||
def test_early_startup_exception_hook_writes_crash_file_under_application_root(self) -> None:
|
||
from config.runtime_layout import ApplicationPaths
|
||
from main import early_startup_crash
|
||
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
app_dir = Path(tmp)
|
||
app_paths = ApplicationPaths.from_root(app_dir)
|
||
|
||
with patch.object(early_startup_crash, "APPLICATION_PATHS", app_paths):
|
||
try:
|
||
raise RuntimeError("boom")
|
||
except RuntimeError:
|
||
exc_type, exc, tb = sys.exc_info()
|
||
assert exc_type is not None
|
||
assert exc is not None
|
||
early_startup_crash.write_early_startup_crash(exc_type, exc, tb)
|
||
|
||
crash_path = app_dir / "user" / "logs" / "crashes" / "early_startup_crash.log"
|
||
text = crash_path.read_text(encoding="utf-8")
|
||
|
||
self.assertIn("RuntimeError: boom", text)
|
||
self.assertIn("early startup crash", text.lower())
|
||
|
||
|
||
class _BaseWindowEvents:
|
||
def nativeEvent(self, event_type, message):
|
||
self.calls.append("base_native")
|
||
return (False, 123)
|
||
|
||
def showMinimized(self) -> None:
|
||
self.calls.append("base_minimize")
|
||
|
||
def changeEvent(self, event) -> None:
|
||
self.calls.append("base_change")
|
||
|
||
def moveEvent(self, event) -> None:
|
||
self.calls.append("base_move")
|
||
|
||
def resizeEvent(self, event) -> None:
|
||
self.calls.append("base_resize")
|
||
|
||
def showEvent(self, event) -> None:
|
||
self.calls.append("base_show")
|
||
|
||
def hideEvent(self, event) -> None:
|
||
self.calls.append("base_hide")
|
||
|
||
def isActiveWindow(self) -> bool:
|
||
return True
|
||
|
||
|
||
class WindowLifecycleEarlyEventTests(unittest.TestCase):
|
||
def test_resize_move_and_show_events_do_not_require_attached_runtime(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
|
||
def findChildren(self, *_args, **_kwargs):
|
||
return []
|
||
|
||
window = Window()
|
||
|
||
window.resizeEvent(object())
|
||
window.moveEvent(object())
|
||
window.showEvent(object())
|
||
|
||
self.assertEqual(window.calls, ["base_resize", "base_move", "base_show"])
|
||
|
||
def test_resize_and_show_events_use_runtime_after_it_is_attached(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.window_geometry_runtime = SimpleNamespace(
|
||
on_geometry_changed=Mock(),
|
||
apply_saved_maximized_state_if_needed=Mock(),
|
||
enable_persistence=Mock(),
|
||
)
|
||
self.window_notification_center = SimpleNamespace(
|
||
schedule_startup_notification_queue=Mock(),
|
||
)
|
||
self.startup_state = SimpleNamespace(
|
||
ttff_logged=False,
|
||
ttff_ms=None,
|
||
)
|
||
self.visual_state = SimpleNamespace(holiday_effects=None)
|
||
|
||
def findChildren(self, *_args, **_kwargs):
|
||
return []
|
||
|
||
window = Window()
|
||
|
||
with patch("main.window_lifecycle.QTimer.singleShot", side_effect=lambda *_args, **_kwargs: None):
|
||
window.resizeEvent(object())
|
||
window.showEvent(object())
|
||
|
||
window.window_geometry_runtime.on_geometry_changed.assert_called_once()
|
||
window.window_geometry_runtime.apply_saved_maximized_state_if_needed.assert_called_once()
|
||
window.window_notification_center.schedule_startup_notification_queue.assert_called_once_with(0)
|
||
self.assertTrue(window.startup_state.ttff_logged)
|
||
|
||
def test_show_event_schedules_titlebar_layout_refresh_after_first_show(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.window_geometry_runtime = SimpleNamespace(
|
||
apply_saved_maximized_state_if_needed=Mock(),
|
||
enable_persistence=Mock(),
|
||
)
|
||
self.window_notification_center = None
|
||
self.startup_state = SimpleNamespace(
|
||
ttff_logged=False,
|
||
ttff_ms=None,
|
||
)
|
||
self.visual_state = SimpleNamespace(holiday_effects=None)
|
||
|
||
def findChildren(self, *_args, **_kwargs):
|
||
return []
|
||
|
||
window = Window()
|
||
scheduled_delays: list[int] = []
|
||
|
||
def record_single_shot(delay, callback):
|
||
scheduled_delays.append(delay)
|
||
|
||
with (
|
||
patch("main.window_lifecycle.refresh_titlebar_layout", create=True) as refresh_titlebar_layout,
|
||
patch("main.window_lifecycle.QTimer.singleShot", side_effect=record_single_shot),
|
||
):
|
||
window.showEvent(object())
|
||
|
||
refresh_titlebar_layout.assert_not_called()
|
||
self.assertIn(0, scheduled_delays)
|
||
self.assertIn(120, scheduled_delays)
|
||
|
||
def test_window_events_pause_and_resume_holiday_animation_by_visibility(self) -> None:
|
||
from PyQt6.QtCore import QEvent
|
||
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Event:
|
||
def __init__(self, event_type) -> None:
|
||
self._event_type = event_type
|
||
|
||
def type(self):
|
||
return self._event_type
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.active = True
|
||
self.visible = True
|
||
self.minimized = False
|
||
self.visual_state = SimpleNamespace(
|
||
holiday_effects=SimpleNamespace(
|
||
sync_geometry=Mock(),
|
||
set_animation_active=Mock(),
|
||
)
|
||
)
|
||
self.window_geometry_runtime = SimpleNamespace(
|
||
on_window_state_change=Mock(),
|
||
apply_saved_maximized_state_if_needed=Mock(),
|
||
enable_persistence=Mock(),
|
||
)
|
||
self.window_notification_center = None
|
||
self.startup_state = SimpleNamespace(ttff_logged=True, ttff_ms=None)
|
||
|
||
def isActiveWindow(self) -> bool:
|
||
return self.active
|
||
|
||
def isVisible(self) -> bool:
|
||
return self.visible
|
||
|
||
def isMinimized(self) -> bool:
|
||
return self.minimized
|
||
|
||
def findChildren(self, *_args, **_kwargs):
|
||
return []
|
||
|
||
window = Window()
|
||
effects = window.visual_state.holiday_effects
|
||
|
||
window.hideEvent(object())
|
||
effects.set_animation_active.assert_called_with(False)
|
||
|
||
effects.set_animation_active.reset_mock()
|
||
window.visible = True
|
||
window.active = True
|
||
window.minimized = False
|
||
with patch("main.window_lifecycle.QTimer.singleShot", side_effect=lambda *_args, **_kwargs: None):
|
||
window.showEvent(object())
|
||
effects.set_animation_active.assert_called_with(True)
|
||
|
||
effects.set_animation_active.reset_mock()
|
||
window.active = False
|
||
window.changeEvent(Event(QEvent.Type.ActivationChange))
|
||
effects.set_animation_active.assert_called_with(False)
|
||
|
||
effects.set_animation_active.reset_mock()
|
||
window.active = True
|
||
window.minimized = True
|
||
window.changeEvent(Event(QEvent.Type.WindowStateChange))
|
||
effects.set_animation_active.assert_called_with(False)
|
||
|
||
effects.set_animation_active.reset_mock()
|
||
window.minimized = False
|
||
window.changeEvent(Event(QEvent.Type.WindowStateChange))
|
||
effects.set_animation_active.assert_called_with(True)
|
||
|
||
def test_native_minimize_command_hides_to_tray_when_mode_includes_minimize(self) -> None:
|
||
from main.window_native_commands import SC_MINIMIZE, WM_SYSCOMMAND
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.close_to_tray = Mock(side_effect=self._close_to_tray)
|
||
|
||
def _close_to_tray(self) -> bool:
|
||
self.calls.append("hide_to_tray")
|
||
return True
|
||
|
||
window_close_flow = SimpleNamespace(
|
||
minimize_to_tray_enabled=Mock(return_value=True),
|
||
)
|
||
|
||
msg = wintypes.MSG()
|
||
msg.message = WM_SYSCOMMAND
|
||
msg.wParam = SC_MINIMIZE
|
||
window = Window()
|
||
|
||
with (
|
||
patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in minimize path"),
|
||
create=True,
|
||
),
|
||
patch("main.window_native_commands.sys.platform", "win32"),
|
||
):
|
||
result = window.nativeEvent(b"windows_generic_MSG", int(addressof(msg)))
|
||
|
||
self.assertEqual(result, (True, 0))
|
||
window.close_to_tray.assert_called_once()
|
||
self.assertEqual(window.calls, ["hide_to_tray"])
|
||
|
||
def test_native_file_drop_is_handled_before_base_window(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
|
||
window = Window()
|
||
message = object()
|
||
|
||
with patch(
|
||
"main.window_lifecycle.handle_native_preset_file_drop",
|
||
return_value=True,
|
||
) as handle_drop:
|
||
result = window.nativeEvent(b"windows_generic_MSG", message)
|
||
|
||
self.assertEqual(result, (True, 0))
|
||
handle_drop.assert_called_once_with(window, message)
|
||
self.assertEqual(window.calls, [])
|
||
|
||
def test_native_minimize_command_uses_normal_window_flow_when_mode_is_normal(self) -> None:
|
||
from main.window_native_commands import SC_MINIMIZE, WM_SYSCOMMAND
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.close_to_tray = Mock(return_value=True)
|
||
|
||
window_close_flow = SimpleNamespace(
|
||
minimize_to_tray_enabled=Mock(return_value=False),
|
||
)
|
||
|
||
msg = wintypes.MSG()
|
||
msg.message = WM_SYSCOMMAND
|
||
msg.wParam = SC_MINIMIZE
|
||
window = Window()
|
||
|
||
with (
|
||
patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in minimize path"),
|
||
create=True,
|
||
),
|
||
patch("main.window_native_commands.sys.platform", "win32"),
|
||
):
|
||
result = window.nativeEvent(b"windows_generic_MSG", int(addressof(msg)))
|
||
|
||
self.assertEqual(result, (False, 123))
|
||
window.close_to_tray.assert_not_called()
|
||
self.assertEqual(window.calls, ["base_native"])
|
||
|
||
def test_show_minimized_hides_to_tray_when_mode_includes_minimize(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.close_to_tray = Mock(side_effect=self._close_to_tray)
|
||
|
||
def _close_to_tray(self) -> bool:
|
||
self.calls.append("hide_to_tray")
|
||
return True
|
||
|
||
window_close_flow = SimpleNamespace(
|
||
minimize_to_tray_enabled=Mock(return_value=True),
|
||
)
|
||
|
||
window = Window()
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in minimize path"),
|
||
create=True,
|
||
):
|
||
window.showMinimized()
|
||
|
||
window.close_to_tray.assert_called_once()
|
||
self.assertEqual(window.calls, ["hide_to_tray"])
|
||
|
||
def test_show_minimized_uses_normal_window_flow_when_mode_is_normal(self) -> None:
|
||
from main.window_lifecycle import WindowLifecycleMixin
|
||
|
||
class Window(WindowLifecycleMixin, _BaseWindowEvents):
|
||
def __init__(self) -> None:
|
||
self.calls: list[str] = []
|
||
self.close_to_tray = Mock(return_value=True)
|
||
|
||
window_close_flow = SimpleNamespace(
|
||
minimize_to_tray_enabled=Mock(return_value=False),
|
||
)
|
||
|
||
window = Window()
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in minimize path"),
|
||
create=True,
|
||
):
|
||
window.showMinimized()
|
||
|
||
window.close_to_tray.assert_not_called()
|
||
self.assertEqual(window.calls, ["base_minimize"])
|
||
|
||
def test_tray_show_window_uses_fast_sidebar_sync_before_show(self) -> None:
|
||
from ui.window_adapter import show_window
|
||
|
||
calls: list[str] = []
|
||
scheduled: list[tuple[int, object]] = []
|
||
geometry_runtime = SimpleNamespace(
|
||
show_from_hidden=Mock(side_effect=lambda: calls.append("show_from_hidden")),
|
||
)
|
||
window = SimpleNamespace(
|
||
window_geometry_runtime=geometry_runtime,
|
||
raise_=Mock(side_effect=lambda: calls.append("raise")),
|
||
activateWindow=Mock(side_effect=lambda: calls.append("activate")),
|
||
)
|
||
|
||
with (
|
||
patch(
|
||
"ui.navigation.sidebar_builder.sync_existing_nav_visibility",
|
||
side_effect=lambda current_window: calls.append("sync_existing_nav"),
|
||
create=True,
|
||
) as sync_existing_nav_visibility,
|
||
patch(
|
||
"ui.navigation.sidebar_builder.sync_nav_visibility",
|
||
side_effect=lambda current_window: calls.append("sync_nav"),
|
||
) as sync_nav_visibility,
|
||
patch(
|
||
"ui.theme_refresh.flush_pending_theme_refreshes",
|
||
side_effect=lambda current_window: calls.append("flush_theme"),
|
||
create=True,
|
||
) as flush_theme_refreshes,
|
||
patch(
|
||
"ui.window_adapter.QTimer",
|
||
SimpleNamespace(
|
||
singleShot=lambda delay, callback: scheduled.append((int(delay), callback)),
|
||
),
|
||
create=True,
|
||
),
|
||
):
|
||
show_window(window)
|
||
|
||
sync_existing_nav_visibility.assert_called_once_with(window)
|
||
sync_nav_visibility.assert_not_called()
|
||
self.assertEqual(
|
||
calls,
|
||
[
|
||
"sync_existing_nav",
|
||
"show_from_hidden",
|
||
"raise",
|
||
"activate",
|
||
],
|
||
)
|
||
self.assertEqual(len(scheduled), 1)
|
||
self.assertEqual(scheduled[0][0], 0)
|
||
|
||
scheduled.pop(0)[1]()
|
||
flush_theme_refreshes.assert_called_once_with(window)
|
||
sync_nav_visibility.assert_called_once_with(window)
|
||
self.assertEqual(calls[-2:], ["flush_theme", "sync_nav"])
|
||
|
||
def test_tray_show_window_delegates_window_state_restore_to_geometry_runtime(self) -> None:
|
||
from ui.window_adapter import show_window
|
||
|
||
calls: list[str] = []
|
||
geometry_runtime = SimpleNamespace(
|
||
show_from_hidden=Mock(side_effect=lambda: calls.append("show_from_hidden")),
|
||
)
|
||
window = SimpleNamespace(
|
||
window_geometry_runtime=geometry_runtime,
|
||
raise_=Mock(side_effect=lambda: calls.append("raise")),
|
||
activateWindow=Mock(side_effect=lambda: calls.append("activate")),
|
||
)
|
||
|
||
with (
|
||
patch("ui.navigation.sidebar_builder.sync_existing_nav_visibility", create=True),
|
||
patch("ui.window_adapter.QTimer", SimpleNamespace(singleShot=lambda *_args, **_kwargs: None), create=True),
|
||
):
|
||
show_window(window)
|
||
|
||
self.assertEqual(
|
||
calls,
|
||
[
|
||
"show_from_hidden",
|
||
"raise",
|
||
"activate",
|
||
],
|
||
)
|
||
|
||
|
||
class WindowsSessionShutdownTests(unittest.TestCase):
|
||
def test_close_flow_receives_launch_state_snapshot_instead_of_full_runtime_feature(self) -> None:
|
||
from main import window_lifecycle_setup
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
init_signature = inspect.signature(WindowCloseFlow.__init__)
|
||
flow_source = inspect.getsource(WindowCloseFlow)
|
||
setup_source = inspect.getsource(window_lifecycle_setup.attach_window_lifecycle)
|
||
|
||
self.assertNotIn("runtime_feature", init_signature.parameters)
|
||
self.assertNotIn("self._runtime =", flow_source)
|
||
self.assertIn("get_launch_state_snapshot", init_signature.parameters)
|
||
self.assertIn("self._get_launch_state_snapshot", flow_source)
|
||
self.assertIn("get_launch_state_snapshot=features.runtime.snapshot", setup_source)
|
||
|
||
def test_close_flow_skips_dialog_when_windows_session_is_ending(self) -> None:
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
close_dialog_module = types.ModuleType("ui.close_dialog")
|
||
close_dialog_module.ask_close_action = Mock(return_value=None)
|
||
original_close_dialog = sys.modules.get("ui.close_dialog")
|
||
sys.modules["ui.close_dialog"] = close_dialog_module
|
||
try:
|
||
event = SimpleNamespace(ignore=Mock())
|
||
close_state = SimpleNamespace(
|
||
closing_completely=False,
|
||
windows_session_ending=True,
|
||
)
|
||
runtime = SimpleNamespace(snapshot=Mock())
|
||
flow = WindowCloseFlow(
|
||
parent=object(),
|
||
close_state=close_state,
|
||
get_launch_state_snapshot=runtime.snapshot,
|
||
close_to_tray=Mock(),
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
)
|
||
|
||
self.assertTrue(flow.should_continue_final_close(event))
|
||
finally:
|
||
if original_close_dialog is None:
|
||
sys.modules.pop("ui.close_dialog", None)
|
||
else:
|
||
sys.modules["ui.close_dialog"] = original_close_dialog
|
||
|
||
event.ignore.assert_not_called()
|
||
close_dialog_module.ask_close_action.assert_not_called()
|
||
runtime.snapshot.assert_not_called()
|
||
|
||
def test_close_flow_hides_to_tray_without_dialog_when_mode_includes_close(self) -> None:
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
close_dialog_module = types.ModuleType("ui.close_dialog")
|
||
close_dialog_module.ask_close_action = Mock(return_value=None)
|
||
original_close_dialog = sys.modules.get("ui.close_dialog")
|
||
sys.modules["ui.close_dialog"] = close_dialog_module
|
||
try:
|
||
event = SimpleNamespace(ignore=Mock())
|
||
close_state = SimpleNamespace(
|
||
closing_completely=False,
|
||
windows_session_ending=False,
|
||
)
|
||
runtime = SimpleNamespace(snapshot=Mock())
|
||
close_to_tray = Mock(return_value=True)
|
||
flow = WindowCloseFlow(
|
||
parent=object(),
|
||
close_state=close_state,
|
||
get_launch_state_snapshot=runtime.snapshot,
|
||
close_to_tray=close_to_tray,
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
tray_close_mode=Mock(return_value="minimize_and_close"),
|
||
)
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in close path"),
|
||
create=True,
|
||
):
|
||
self.assertFalse(flow.should_continue_final_close(event))
|
||
finally:
|
||
if original_close_dialog is None:
|
||
sys.modules.pop("ui.close_dialog", None)
|
||
else:
|
||
sys.modules["ui.close_dialog"] = original_close_dialog
|
||
|
||
event.ignore.assert_called_once()
|
||
close_to_tray.assert_called_once()
|
||
close_dialog_module.ask_close_action.assert_not_called()
|
||
runtime.snapshot.assert_not_called()
|
||
|
||
def test_close_flow_shows_dialog_when_mode_is_normal(self) -> None:
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
close_dialog_module = types.ModuleType("ui.close_dialog")
|
||
close_dialog_module.ask_close_action = Mock(return_value=None)
|
||
original_close_dialog = sys.modules.get("ui.close_dialog")
|
||
sys.modules["ui.close_dialog"] = close_dialog_module
|
||
try:
|
||
event = SimpleNamespace(ignore=Mock())
|
||
close_state = SimpleNamespace(
|
||
closing_completely=False,
|
||
windows_session_ending=False,
|
||
)
|
||
runtime = SimpleNamespace(snapshot=Mock(return_value=SimpleNamespace(running=True)))
|
||
close_to_tray = Mock(return_value=True)
|
||
flow = WindowCloseFlow(
|
||
parent=object(),
|
||
close_state=close_state,
|
||
get_launch_state_snapshot=runtime.snapshot,
|
||
close_to_tray=close_to_tray,
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
tray_close_mode=Mock(return_value="normal"),
|
||
)
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in close path"),
|
||
create=True,
|
||
):
|
||
self.assertFalse(flow.should_continue_final_close(event))
|
||
finally:
|
||
if original_close_dialog is None:
|
||
sys.modules.pop("ui.close_dialog", None)
|
||
else:
|
||
sys.modules["ui.close_dialog"] = original_close_dialog
|
||
|
||
event.ignore.assert_called_once()
|
||
runtime.snapshot.assert_called_once()
|
||
close_dialog_module.ask_close_action.assert_called_once_with(
|
||
parent=flow._parent,
|
||
launch_running=True,
|
||
)
|
||
close_to_tray.assert_not_called()
|
||
|
||
def test_close_flow_shows_dialog_when_only_minimize_hides_to_tray(self) -> None:
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
close_dialog_module = types.ModuleType("ui.close_dialog")
|
||
close_dialog_module.ask_close_action = Mock(return_value=None)
|
||
original_close_dialog = sys.modules.get("ui.close_dialog")
|
||
sys.modules["ui.close_dialog"] = close_dialog_module
|
||
try:
|
||
event = SimpleNamespace(ignore=Mock())
|
||
close_state = SimpleNamespace(
|
||
closing_completely=False,
|
||
windows_session_ending=False,
|
||
)
|
||
runtime = SimpleNamespace(snapshot=Mock(return_value=SimpleNamespace(launch_running=True)))
|
||
close_to_tray = Mock(return_value=True)
|
||
flow = WindowCloseFlow(
|
||
parent=object(),
|
||
close_state=close_state,
|
||
get_launch_state_snapshot=runtime.snapshot,
|
||
close_to_tray=close_to_tray,
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
tray_close_mode=Mock(return_value="minimize_only"),
|
||
)
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in close path"),
|
||
create=True,
|
||
):
|
||
self.assertFalse(flow.should_continue_final_close(event))
|
||
finally:
|
||
if original_close_dialog is None:
|
||
sys.modules.pop("ui.close_dialog", None)
|
||
else:
|
||
sys.modules["ui.close_dialog"] = original_close_dialog
|
||
|
||
event.ignore.assert_called_once()
|
||
runtime.snapshot.assert_called_once()
|
||
close_dialog_module.ask_close_action.assert_called_once_with(
|
||
parent=flow._parent,
|
||
launch_running=True,
|
||
)
|
||
close_to_tray.assert_not_called()
|
||
|
||
def test_close_flow_still_shows_dialog_when_runtime_snapshot_fails(self) -> None:
|
||
from ui.window_close_flow import WindowCloseFlow
|
||
|
||
close_dialog_module = types.ModuleType("ui.close_dialog")
|
||
close_dialog_module.ask_close_action = Mock(return_value=None)
|
||
original_close_dialog = sys.modules.get("ui.close_dialog")
|
||
sys.modules["ui.close_dialog"] = close_dialog_module
|
||
try:
|
||
event = SimpleNamespace(ignore=Mock())
|
||
close_state = SimpleNamespace(
|
||
closing_completely=False,
|
||
windows_session_ending=False,
|
||
)
|
||
runtime = SimpleNamespace(snapshot=Mock(side_effect=RuntimeError("snapshot failed")))
|
||
flow = WindowCloseFlow(
|
||
parent=object(),
|
||
close_state=close_state,
|
||
get_launch_state_snapshot=runtime.snapshot,
|
||
close_to_tray=Mock(),
|
||
exit_stop_dpi=Mock(),
|
||
exit_keep_dpi=Mock(),
|
||
tray_close_mode=Mock(return_value="normal"),
|
||
)
|
||
|
||
with patch(
|
||
"settings.store.get_tray_close_mode",
|
||
side_effect=AssertionError("settings read must not happen in close path"),
|
||
create=True,
|
||
):
|
||
self.assertFalse(flow.should_continue_final_close(event))
|
||
finally:
|
||
if original_close_dialog is None:
|
||
sys.modules.pop("ui.close_dialog", None)
|
||
else:
|
||
sys.modules["ui.close_dialog"] = original_close_dialog
|
||
|
||
event.ignore.assert_called_once()
|
||
runtime.snapshot.assert_called_once()
|
||
close_dialog_module.ask_close_action.assert_called_once_with(
|
||
parent=flow._parent,
|
||
launch_running=False,
|
||
)
|
||
|
||
def test_session_shutdown_attempts_fast_dpi_stop_and_quits_without_second_stop(self) -> None:
|
||
from main.application_lifecycle import ApplicationLifecycle
|
||
|
||
close_state = SimpleNamespace(
|
||
is_exiting=False,
|
||
stop_dpi_on_exit=True,
|
||
closing_completely=False,
|
||
windows_session_ending=False,
|
||
)
|
||
window_port = SimpleNamespace(persist_geometry=Mock(), persist_sidebar_state=Mock())
|
||
runtime_feature = SimpleNamespace(shutdown_sync=Mock())
|
||
lifecycle = ApplicationLifecycle(
|
||
window_port=window_port,
|
||
close_state=close_state,
|
||
runtime_feature=runtime_feature,
|
||
premium_feature=object(),
|
||
telegram_proxy_feature=object(),
|
||
tray_feature=SimpleNamespace(hide_icon_for_exit=Mock()),
|
||
)
|
||
|
||
with patch("main.application_lifecycle.QApplication") as q_application:
|
||
lifecycle.exit_for_windows_session_end()
|
||
|
||
self.assertTrue(close_state.is_exiting)
|
||
self.assertTrue(close_state.closing_completely)
|
||
self.assertTrue(close_state.windows_session_ending)
|
||
self.assertFalse(close_state.stop_dpi_on_exit)
|
||
window_port.persist_geometry.assert_called_once_with(
|
||
context="windows_session_end",
|
||
level="DEBUG",
|
||
)
|
||
window_port.persist_sidebar_state.assert_called_once_with(
|
||
context="windows_session_end",
|
||
level="DEBUG",
|
||
)
|
||
runtime_feature.shutdown_sync.assert_called_once_with(
|
||
reason="windows_session_end",
|
||
include_cleanup=False,
|
||
cleanup_services=False,
|
||
update_runtime_state=False,
|
||
)
|
||
q_application.closeAllWindows.assert_called_once()
|
||
q_application.processEvents.assert_not_called()
|
||
q_application.quit.assert_called_once()
|
||
|
||
def test_final_close_does_not_repeat_dpi_shutdown_or_probe(self) -> None:
|
||
from main.application_lifecycle import ApplicationLifecycle
|
||
|
||
close_state = SimpleNamespace(
|
||
is_exiting=True,
|
||
stop_dpi_on_exit=True,
|
||
closing_completely=True,
|
||
windows_session_ending=False,
|
||
)
|
||
window_port = SimpleNamespace(
|
||
persist_geometry=Mock(),
|
||
persist_sidebar_state=Mock(),
|
||
cleanup_theme=Mock(),
|
||
cleanup_threaded_pages=Mock(),
|
||
cleanup_visual_and_proxy_resources=Mock(),
|
||
)
|
||
runtime_feature = SimpleNamespace(
|
||
cleanup_process_monitor=Mock(),
|
||
cleanup_threads=Mock(),
|
||
is_any_running=Mock(return_value=False),
|
||
shutdown_sync=Mock(),
|
||
)
|
||
premium_feature = SimpleNamespace(cleanup_subscription=Mock())
|
||
telegram_proxy_feature = SimpleNamespace(cleanup=Mock())
|
||
tray_feature = SimpleNamespace(cleanup=Mock())
|
||
lifecycle = ApplicationLifecycle(
|
||
window_port=window_port,
|
||
close_state=close_state,
|
||
runtime_feature=runtime_feature,
|
||
premium_feature=premium_feature,
|
||
telegram_proxy_feature=telegram_proxy_feature,
|
||
tray_feature=tray_feature,
|
||
)
|
||
|
||
lifecycle.run_final_close_cleanup()
|
||
|
||
runtime_feature.is_any_running.assert_not_called()
|
||
runtime_feature.shutdown_sync.assert_not_called()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|