synodic-client 0.0.1.dev53__tar.gz → 0.0.1.dev55__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/schema.py +19 -1
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/settings.py +16 -30
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/tray.py +1 -2
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/update_controller.py +61 -55
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_settings.py +8 -16
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_controller.py +141 -104
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/README.md +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_startup.py +0 -0
|
@@ -13,6 +13,7 @@ from collections.abc import Callable
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from enum import Enum, auto
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from typing import Protocol, runtime_checkable
|
|
16
17
|
|
|
17
18
|
from porringer.schema import (
|
|
18
19
|
PluginInfo,
|
|
@@ -336,10 +337,27 @@ class _DispatchState:
|
|
|
336
337
|
|
|
337
338
|
|
|
338
339
|
# ---------------------------------------------------------------------------
|
|
339
|
-
# Update banner data models
|
|
340
|
+
# Update view protocol & banner data models
|
|
340
341
|
# ---------------------------------------------------------------------------
|
|
341
342
|
|
|
342
343
|
|
|
344
|
+
@runtime_checkable
|
|
345
|
+
class UpdateView(Protocol):
|
|
346
|
+
"""Minimal display contract for the self-update lifecycle.
|
|
347
|
+
|
|
348
|
+
:class:`UpdateBanner` satisfies this protocol implicitly via
|
|
349
|
+
structural typing. The controller broadcasts state transitions
|
|
350
|
+
through a ``list[UpdateView]`` so that every window showing update
|
|
351
|
+
status stays in sync.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def show_downloading(self, version: str) -> None: ...
|
|
355
|
+
def show_downloading_progress(self, percentage: int) -> None: ...
|
|
356
|
+
def show_ready(self, version: str) -> None: ...
|
|
357
|
+
def show_error(self, message: str) -> None: ...
|
|
358
|
+
def hide_banner(self) -> None: ...
|
|
359
|
+
|
|
360
|
+
|
|
343
361
|
class UpdateBannerState(Enum):
|
|
344
362
|
"""Visual states for the update banner."""
|
|
345
363
|
|
|
@@ -31,7 +31,8 @@ from PySide6.QtWidgets import (
|
|
|
31
31
|
from synodic_client.application.icon import app_icon
|
|
32
32
|
from synodic_client.application.screen import _format_relative_time
|
|
33
33
|
from synodic_client.application.screen.card import CardFrame
|
|
34
|
-
from synodic_client.application.
|
|
34
|
+
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
35
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
35
36
|
from synodic_client.logging import log_path, set_debug_level
|
|
36
37
|
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
37
38
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
@@ -54,9 +55,6 @@ class SettingsWindow(QMainWindow):
|
|
|
54
55
|
check_updates_requested = Signal()
|
|
55
56
|
"""Emitted when the user clicks the *Check for Updates* button."""
|
|
56
57
|
|
|
57
|
-
restart_requested = Signal()
|
|
58
|
-
"""Emitted when the user clicks the *Restart & Update* button."""
|
|
59
|
-
|
|
60
58
|
def showEvent(self, event: QShowEvent) -> None: # noqa: N802
|
|
61
59
|
"""[DIAG] Log every show event with a stack trace."""
|
|
62
60
|
geo = self.geometry()
|
|
@@ -199,18 +197,13 @@ class SettingsWindow(QMainWindow):
|
|
|
199
197
|
self._check_updates_btn = QPushButton('Check for Updates\u2026')
|
|
200
198
|
self._check_updates_btn.clicked.connect(self._on_check_updates_clicked)
|
|
201
199
|
row.addWidget(self._check_updates_btn)
|
|
202
|
-
self._update_status_label = QLabel('')
|
|
203
|
-
self._update_status_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
204
|
-
row.addWidget(self._update_status_label)
|
|
205
|
-
|
|
206
|
-
self._restart_btn = QPushButton('Restart \u0026 Update')
|
|
207
|
-
self._restart_btn.clicked.connect(self.restart_requested.emit)
|
|
208
|
-
self._restart_btn.hide()
|
|
209
|
-
row.addWidget(self._restart_btn)
|
|
210
|
-
|
|
211
200
|
row.addStretch()
|
|
212
201
|
content.addLayout(row)
|
|
213
202
|
|
|
203
|
+
# Embedded update banner (same widget used in the main window)
|
|
204
|
+
self._update_banner = UpdateBanner()
|
|
205
|
+
content.addWidget(self._update_banner)
|
|
206
|
+
|
|
214
207
|
# Last client update timestamp
|
|
215
208
|
self._last_client_update_label = QLabel('')
|
|
216
209
|
self._last_client_update_label.setStyleSheet('color: #808080; font-size: 11px;')
|
|
@@ -280,31 +273,25 @@ class SettingsWindow(QMainWindow):
|
|
|
280
273
|
else:
|
|
281
274
|
self._last_client_update_label.setText('')
|
|
282
275
|
|
|
283
|
-
|
|
284
|
-
|
|
276
|
+
@property
|
|
277
|
+
def update_banner(self) -> UpdateBanner:
|
|
278
|
+
"""The embedded :class:`UpdateBanner` for this window."""
|
|
279
|
+
return self._update_banner
|
|
285
280
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
self.
|
|
291
|
-
self._update_status_label.setStyleSheet(style)
|
|
281
|
+
def set_last_updated(self, timestamp: str) -> None:
|
|
282
|
+
"""Refresh the *Last updated* label from a raw ISO timestamp."""
|
|
283
|
+
relative = _format_relative_time(timestamp)
|
|
284
|
+
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
285
|
+
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
292
286
|
|
|
293
287
|
def set_checking(self) -> None:
|
|
294
|
-
"""Enter the *checking* state — disable button
|
|
288
|
+
"""Enter the *checking* state — disable button."""
|
|
295
289
|
self._check_updates_btn.setEnabled(False)
|
|
296
|
-
self._restart_btn.hide()
|
|
297
|
-
self._update_status_label.setText('Checking\u2026')
|
|
298
|
-
self._update_status_label.setStyleSheet(UPDATE_STATUS_CHECKING_STYLE)
|
|
299
290
|
|
|
300
291
|
def reset_check_updates_button(self) -> None:
|
|
301
292
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
302
293
|
self._check_updates_btn.setEnabled(True)
|
|
303
294
|
|
|
304
|
-
def show_restart_button(self) -> None:
|
|
305
|
-
"""Show the *Restart & Update* button."""
|
|
306
|
-
self._restart_btn.show()
|
|
307
|
-
|
|
308
295
|
def show(self) -> None:
|
|
309
296
|
"""Sync controls from config, then show the window."""
|
|
310
297
|
self.sync_from_config()
|
|
@@ -350,7 +337,6 @@ class SettingsWindow(QMainWindow):
|
|
|
350
337
|
def _on_check_updates_clicked(self) -> None:
|
|
351
338
|
"""Handle the *Check for Updates* button click."""
|
|
352
339
|
self._check_updates_btn.setEnabled(False)
|
|
353
|
-
self._update_status_label.setText('Checking\u2026')
|
|
354
340
|
self.check_updates_requested.emit()
|
|
355
341
|
|
|
356
342
|
def _on_channel_changed(self, index: int) -> None:
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -68,11 +68,10 @@ class TrayScreen:
|
|
|
68
68
|
window.settings_requested.connect(self._show_settings)
|
|
69
69
|
|
|
70
70
|
# Update controller - owns the self-update lifecycle & timer
|
|
71
|
-
self._banner = window.update_banner
|
|
72
71
|
self._update_controller = UpdateController(
|
|
73
72
|
app,
|
|
74
73
|
client,
|
|
75
|
-
self.
|
|
74
|
+
[window.update_banner, self._settings_window.update_banner],
|
|
76
75
|
settings_window=self._settings_window,
|
|
77
76
|
config=config,
|
|
78
77
|
)
|
|
@@ -17,12 +17,8 @@ from typing import TYPE_CHECKING
|
|
|
17
17
|
from PySide6.QtCore import QTimer
|
|
18
18
|
from PySide6.QtWidgets import QApplication
|
|
19
19
|
|
|
20
|
+
from synodic_client.application.screen.schema import UpdateView
|
|
20
21
|
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
21
|
-
from synodic_client.application.theme import (
|
|
22
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
23
|
-
UPDATE_STATUS_ERROR_STYLE,
|
|
24
|
-
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
25
|
-
)
|
|
26
22
|
from synodic_client.application.workers import check_for_update, download_update
|
|
27
23
|
from synodic_client.resolution import (
|
|
28
24
|
ResolvedConfig,
|
|
@@ -48,22 +44,20 @@ class UpdateController:
|
|
|
48
44
|
The running ``QApplication`` (needed for ``quit()`` on auto-apply).
|
|
49
45
|
client:
|
|
50
46
|
The Synodic Client service facade.
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
views:
|
|
48
|
+
One or more :class:`UpdateView` implementations to broadcast
|
|
49
|
+
state transitions to (typically ``UpdateBanner`` instances).
|
|
53
50
|
settings_window:
|
|
54
|
-
The ``SettingsWindow`` (
|
|
51
|
+
The ``SettingsWindow`` (check button + last-updated label).
|
|
55
52
|
config:
|
|
56
53
|
Optional pre-resolved configuration. ``None`` resolves from disk.
|
|
57
|
-
is_user_active:
|
|
58
|
-
Predicate returning ``True`` when the user has a visible window.
|
|
59
|
-
Auto-apply is deferred while active; checks still run normally.
|
|
60
54
|
"""
|
|
61
55
|
|
|
62
56
|
def __init__(
|
|
63
57
|
self,
|
|
64
58
|
app: QApplication,
|
|
65
59
|
client: Client,
|
|
66
|
-
|
|
60
|
+
views: list[UpdateView],
|
|
67
61
|
*,
|
|
68
62
|
settings_window: SettingsWindow,
|
|
69
63
|
config: ResolvedConfig | None = None,
|
|
@@ -73,17 +67,18 @@ class UpdateController:
|
|
|
73
67
|
Args:
|
|
74
68
|
app: The running ``QApplication``.
|
|
75
69
|
client: The Synodic Client service facade.
|
|
76
|
-
|
|
77
|
-
settings_window: The settings window
|
|
70
|
+
views: One or more :class:`UpdateView` implementations.
|
|
71
|
+
settings_window: The settings window (check button + timestamp).
|
|
78
72
|
config: Optional pre-resolved configuration.
|
|
79
73
|
"""
|
|
80
74
|
self._app = app
|
|
81
75
|
self._client = client
|
|
82
|
-
self.
|
|
76
|
+
self._views = views
|
|
83
77
|
self._settings_window = settings_window
|
|
84
78
|
self._config = config
|
|
85
79
|
self._is_user_active: Callable[[], bool] = lambda: False
|
|
86
80
|
self._update_task: asyncio.Task[None] | None = None
|
|
81
|
+
self._pending_version: str | None = None
|
|
87
82
|
|
|
88
83
|
# Derive auto-apply preference from config
|
|
89
84
|
resolved = self._resolve_config()
|
|
@@ -93,13 +88,14 @@ class UpdateController:
|
|
|
93
88
|
self._auto_update_timer: QTimer | None = None
|
|
94
89
|
self._restart_auto_update_timer()
|
|
95
90
|
|
|
96
|
-
# Wire banner signals
|
|
97
|
-
self.
|
|
98
|
-
|
|
91
|
+
# Wire banner signals (UpdateBanner-specific, outside the protocol)
|
|
92
|
+
for view in self._views:
|
|
93
|
+
if isinstance(view, UpdateBanner):
|
|
94
|
+
view.restart_requested.connect(self._apply_update)
|
|
95
|
+
view.retry_requested.connect(lambda: self.check_now(silent=True))
|
|
99
96
|
|
|
100
97
|
# Wire settings check-updates button
|
|
101
98
|
self._settings_window.check_updates_requested.connect(self._on_manual_check)
|
|
102
|
-
self._settings_window.restart_requested.connect(self._apply_update)
|
|
103
99
|
|
|
104
100
|
def set_user_active_predicate(self, predicate: Callable[[], bool]) -> None:
|
|
105
101
|
"""Set the predicate used to defer auto-apply when the user is active.
|
|
@@ -211,11 +207,13 @@ class UpdateController:
|
|
|
211
207
|
"""Run an update check."""
|
|
212
208
|
if self._client.updater is None:
|
|
213
209
|
if not silent:
|
|
214
|
-
|
|
210
|
+
for view in self._views:
|
|
211
|
+
view.show_error('Updater is not initialized.')
|
|
215
212
|
return
|
|
216
213
|
|
|
217
|
-
#
|
|
218
|
-
self.
|
|
214
|
+
# Preserve the banner state when an update is already pending
|
|
215
|
+
if self._pending_version is None:
|
|
216
|
+
self._settings_window.set_checking()
|
|
219
217
|
|
|
220
218
|
self._update_task = asyncio.create_task(self._async_check(silent=silent))
|
|
221
219
|
|
|
@@ -235,45 +233,47 @@ class UpdateController:
|
|
|
235
233
|
self._settings_window.reset_check_updates_button()
|
|
236
234
|
|
|
237
235
|
if result is None:
|
|
238
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
239
236
|
if not silent:
|
|
240
|
-
|
|
237
|
+
for view in self._views:
|
|
238
|
+
view.show_error('Failed to check for updates.')
|
|
241
239
|
else:
|
|
242
240
|
logger.warning('Automatic update check failed (no result)')
|
|
243
241
|
return
|
|
244
242
|
|
|
245
243
|
if result.error:
|
|
246
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
247
244
|
if not silent:
|
|
248
|
-
self.
|
|
245
|
+
for view in self._views:
|
|
246
|
+
view.show_error(result.error)
|
|
249
247
|
else:
|
|
250
248
|
logger.warning('Automatic update check failed: %s', result.error)
|
|
251
249
|
return
|
|
252
250
|
|
|
253
251
|
if not result.available:
|
|
254
|
-
self._settings_window.set_update_status('Up to date', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
255
252
|
if not silent:
|
|
256
253
|
logger.info('No updates available (current: %s)', result.current_version)
|
|
257
254
|
else:
|
|
258
255
|
logger.debug('Automatic update check: no update available')
|
|
259
256
|
return
|
|
260
257
|
|
|
261
|
-
# Update available — show status and start download
|
|
262
258
|
version = str(result.latest_version)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
259
|
+
|
|
260
|
+
# Already downloaded — restore the ready state without re-downloading
|
|
261
|
+
if version == self._pending_version:
|
|
262
|
+
self._show_ready(version)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# New update available — download it
|
|
266
|
+
for view in self._views:
|
|
267
|
+
view.show_downloading(version)
|
|
268
268
|
self._start_download(version)
|
|
269
269
|
|
|
270
270
|
def _on_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
271
271
|
"""Handle unexpected exception during update check."""
|
|
272
272
|
self._settings_window.reset_check_updates_button()
|
|
273
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
274
273
|
|
|
275
274
|
if not silent:
|
|
276
|
-
self.
|
|
275
|
+
for view in self._views:
|
|
276
|
+
view.show_error(f'Update check error: {error}')
|
|
277
277
|
else:
|
|
278
278
|
logger.warning('Automatic update check error: %s', error)
|
|
279
279
|
|
|
@@ -290,45 +290,49 @@ class UpdateController:
|
|
|
290
290
|
try:
|
|
291
291
|
success = await download_update(
|
|
292
292
|
self._client,
|
|
293
|
-
on_progress=self.
|
|
293
|
+
on_progress=self._on_download_progress,
|
|
294
294
|
)
|
|
295
295
|
self._on_download_finished(success, version)
|
|
296
296
|
except Exception as exc:
|
|
297
297
|
logger.exception('Update download failed')
|
|
298
298
|
self._on_download_error(str(exc))
|
|
299
299
|
|
|
300
|
+
def _on_download_progress(self, percentage: int) -> None:
|
|
301
|
+
"""Broadcast download progress to all views."""
|
|
302
|
+
for view in self._views:
|
|
303
|
+
view.show_downloading_progress(percentage)
|
|
304
|
+
|
|
300
305
|
def _on_download_finished(self, success: bool, version: str) -> None:
|
|
301
306
|
"""Handle download completion."""
|
|
302
307
|
if not success:
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
for view in self._views:
|
|
309
|
+
view.show_error('Download failed. Please try again later.')
|
|
305
310
|
return
|
|
306
311
|
|
|
307
|
-
# Persist the client update timestamp
|
|
308
|
-
|
|
312
|
+
# Persist and display the client update timestamp
|
|
313
|
+
ts = datetime.now(UTC).isoformat()
|
|
314
|
+
update_user_config(last_client_update=ts)
|
|
315
|
+
self._settings_window.set_last_updated(ts)
|
|
316
|
+
|
|
317
|
+
self._pending_version = version
|
|
309
318
|
|
|
310
319
|
if self._can_auto_apply():
|
|
311
320
|
# Silently apply and restart — no banner, no user interaction
|
|
312
321
|
logger.info('Auto-applying update v%s', version)
|
|
313
|
-
self._settings_window.set_update_status(
|
|
314
|
-
f'v{version} installing\u2026',
|
|
315
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
316
|
-
)
|
|
317
322
|
self._apply_update(silent=True)
|
|
318
323
|
return
|
|
319
324
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
self._settings_window.show_restart_button()
|
|
325
|
+
self._show_ready(version)
|
|
326
|
+
|
|
327
|
+
def _show_ready(self, version: str) -> None:
|
|
328
|
+
"""Present the *ready to restart* state across all views."""
|
|
329
|
+
for view in self._views:
|
|
330
|
+
view.show_ready(version)
|
|
327
331
|
|
|
328
332
|
def _on_download_error(self, error: str) -> None:
|
|
329
|
-
"""Handle download error — show error
|
|
330
|
-
self.
|
|
331
|
-
|
|
333
|
+
"""Handle download error — show error across all views."""
|
|
334
|
+
for view in self._views:
|
|
335
|
+
view.show_error(f'Download error: {error}')
|
|
332
336
|
|
|
333
337
|
# ------------------------------------------------------------------
|
|
334
338
|
# Apply
|
|
@@ -345,9 +349,11 @@ class UpdateController:
|
|
|
345
349
|
return
|
|
346
350
|
|
|
347
351
|
try:
|
|
352
|
+
self._pending_version = None
|
|
348
353
|
self._client.apply_update_on_exit(restart=True, silent=silent)
|
|
349
354
|
logger.info('Update scheduled — restarting application')
|
|
350
355
|
self._app.quit()
|
|
351
356
|
except Exception as e:
|
|
352
357
|
logger.error('Failed to apply update: %s', e)
|
|
353
|
-
|
|
358
|
+
for view in self._views:
|
|
359
|
+
view.show_error(f'Failed to apply update: {e}')
|
|
@@ -334,16 +334,18 @@ class TestSyncDoesNotEmit:
|
|
|
334
334
|
|
|
335
335
|
|
|
336
336
|
class TestCheckForUpdatesButton:
|
|
337
|
-
"""Verify the Check for Updates button and
|
|
337
|
+
"""Verify the Check for Updates button and embedded update banner."""
|
|
338
338
|
|
|
339
339
|
@staticmethod
|
|
340
|
-
def
|
|
341
|
-
"""Window has the check-updates button and
|
|
340
|
+
def test_button_and_banner_exist() -> None:
|
|
341
|
+
"""Window has the check-updates button and an embedded UpdateBanner."""
|
|
342
342
|
window = _make_window()
|
|
343
343
|
assert hasattr(window, '_check_updates_btn')
|
|
344
|
-
assert hasattr(window, '_update_status_label')
|
|
345
344
|
assert window._check_updates_btn.text() == 'Check for Updates\u2026'
|
|
346
|
-
assert
|
|
345
|
+
assert hasattr(window, '_update_banner')
|
|
346
|
+
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
347
|
+
|
|
348
|
+
assert isinstance(window.update_banner, UpdateBanner)
|
|
347
349
|
|
|
348
350
|
@staticmethod
|
|
349
351
|
def test_click_emits_signal_and_disables() -> None:
|
|
@@ -356,15 +358,6 @@ class TestCheckForUpdatesButton:
|
|
|
356
358
|
|
|
357
359
|
signal_spy.assert_called_once()
|
|
358
360
|
assert window._check_updates_btn.isEnabled() is False
|
|
359
|
-
assert window._update_status_label.text() == 'Checking\u2026'
|
|
360
|
-
|
|
361
|
-
@staticmethod
|
|
362
|
-
def test_set_update_status() -> None:
|
|
363
|
-
"""set_update_status sets the label text and style."""
|
|
364
|
-
window = _make_window()
|
|
365
|
-
window.set_update_status('Up to date', 'color: green;')
|
|
366
|
-
assert window._update_status_label.text() == 'Up to date'
|
|
367
|
-
assert 'green' in window._update_status_label.styleSheet()
|
|
368
361
|
|
|
369
362
|
@staticmethod
|
|
370
363
|
def test_reset_check_updates_button() -> None:
|
|
@@ -378,8 +371,7 @@ class TestCheckForUpdatesButton:
|
|
|
378
371
|
|
|
379
372
|
@staticmethod
|
|
380
373
|
def test_set_checking() -> None:
|
|
381
|
-
"""set_checking disables the button
|
|
374
|
+
"""set_checking disables the button."""
|
|
382
375
|
window = _make_window()
|
|
383
376
|
window.set_checking()
|
|
384
377
|
assert window._check_updates_btn.isEnabled() is False
|
|
385
|
-
assert window._update_status_label.text() == 'Checking\u2026'
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -8,11 +8,6 @@ from unittest.mock import MagicMock, patch
|
|
|
8
8
|
from packaging.version import Version
|
|
9
9
|
|
|
10
10
|
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
11
|
-
from synodic_client.application.theme import (
|
|
12
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
13
|
-
UPDATE_STATUS_ERROR_STYLE,
|
|
14
|
-
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
15
|
-
)
|
|
16
11
|
from synodic_client.application.update_controller import UpdateController
|
|
17
12
|
from synodic_client.resolution import ResolvedConfig
|
|
18
13
|
from synodic_client.schema import (
|
|
@@ -51,10 +46,10 @@ def _make_controller(
|
|
|
51
46
|
auto_apply: bool = True,
|
|
52
47
|
auto_update_interval_minutes: int = 0,
|
|
53
48
|
is_user_active: bool = False,
|
|
54
|
-
) -> tuple[UpdateController, MagicMock, MagicMock, UpdateBanner, MagicMock]:
|
|
49
|
+
) -> tuple[UpdateController, MagicMock, MagicMock, UpdateBanner, UpdateBanner, MagicMock]:
|
|
55
50
|
"""Build an ``UpdateController`` with mocked collaborators.
|
|
56
51
|
|
|
57
|
-
Returns (controller, app_mock, client_mock,
|
|
52
|
+
Returns (controller, app_mock, client_mock, banner1, banner2, settings_mock).
|
|
58
53
|
"""
|
|
59
54
|
config = _make_config(
|
|
60
55
|
auto_apply=auto_apply,
|
|
@@ -64,8 +59,11 @@ def _make_controller(
|
|
|
64
59
|
app = MagicMock()
|
|
65
60
|
client = MagicMock()
|
|
66
61
|
client.updater = MagicMock()
|
|
67
|
-
|
|
62
|
+
banner1 = UpdateBanner()
|
|
63
|
+
banner2 = UpdateBanner()
|
|
68
64
|
settings = MagicMock()
|
|
65
|
+
# settings mock only needs: check_updates_requested, set_checking,
|
|
66
|
+
# reset_check_updates_button, set_last_updated
|
|
69
67
|
|
|
70
68
|
with patch('synodic_client.application.update_controller.resolve_update_config') as mock_ucfg:
|
|
71
69
|
mock_ucfg.return_value = MagicMock(
|
|
@@ -74,13 +72,13 @@ def _make_controller(
|
|
|
74
72
|
controller = UpdateController(
|
|
75
73
|
app,
|
|
76
74
|
client,
|
|
77
|
-
|
|
75
|
+
[banner1, banner2],
|
|
78
76
|
settings_window=settings,
|
|
79
77
|
config=config,
|
|
80
78
|
)
|
|
81
79
|
controller.set_user_active_predicate(lambda: is_user_active)
|
|
82
80
|
|
|
83
|
-
return controller, app, client,
|
|
81
|
+
return controller, app, client, banner1, banner2, settings
|
|
84
82
|
|
|
85
83
|
|
|
86
84
|
# ---------------------------------------------------------------------------
|
|
@@ -92,61 +90,55 @@ class TestCheckFinished:
|
|
|
92
90
|
"""Verify _on_check_finished routes results correctly."""
|
|
93
91
|
|
|
94
92
|
@staticmethod
|
|
95
|
-
def
|
|
96
|
-
"""A None result should
|
|
97
|
-
ctrl, _app, _client,
|
|
93
|
+
def test_none_result_shows_error_banner_when_not_silent() -> None:
|
|
94
|
+
"""A None result with silent=False should show error on all banners."""
|
|
95
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
98
96
|
ctrl._on_check_finished(None, silent=False)
|
|
99
97
|
|
|
100
98
|
settings.reset_check_updates_button.assert_called_once()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def test_none_result_shows_banner_when_not_silent() -> None:
|
|
105
|
-
"""A None result with silent=False should show the error banner."""
|
|
106
|
-
ctrl, _app, _client, banner, settings = _make_controller()
|
|
107
|
-
ctrl._on_check_finished(None, silent=False)
|
|
108
|
-
|
|
109
|
-
assert banner.state.name == 'ERROR'
|
|
99
|
+
assert b1.state.name == 'ERROR'
|
|
100
|
+
assert b2.state.name == 'ERROR'
|
|
110
101
|
|
|
111
102
|
@staticmethod
|
|
112
103
|
def test_none_result_no_banner_when_silent() -> None:
|
|
113
104
|
"""A None result with silent=True should NOT show the error banner."""
|
|
114
|
-
ctrl, _app, _client,
|
|
105
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
115
106
|
ctrl._on_check_finished(None, silent=True)
|
|
116
107
|
|
|
117
|
-
assert
|
|
108
|
+
assert b1.state.name == 'HIDDEN'
|
|
109
|
+
assert b2.state.name == 'HIDDEN'
|
|
118
110
|
|
|
119
111
|
@staticmethod
|
|
120
|
-
def
|
|
121
|
-
"""An error result should
|
|
122
|
-
ctrl, _app, _client,
|
|
112
|
+
def test_error_result_shows_error_banner() -> None:
|
|
113
|
+
"""An error result should show error on all banners."""
|
|
114
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
123
115
|
result = UpdateInfo(available=False, current_version=Version('1.0.0'), error='No releases found')
|
|
124
116
|
ctrl._on_check_finished(result, silent=False)
|
|
125
117
|
|
|
126
|
-
|
|
118
|
+
assert b1.state.name == 'ERROR'
|
|
119
|
+
assert b2.state.name == 'ERROR'
|
|
127
120
|
|
|
128
121
|
@staticmethod
|
|
129
|
-
def
|
|
130
|
-
"""No update available should
|
|
131
|
-
ctrl, _app, _client,
|
|
122
|
+
def test_no_update_available_banners_remain_hidden() -> None:
|
|
123
|
+
"""No update available should leave banners hidden."""
|
|
124
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
132
125
|
result = UpdateInfo(available=False, current_version=Version('1.0.0'))
|
|
133
126
|
ctrl._on_check_finished(result, silent=False)
|
|
134
127
|
|
|
135
|
-
|
|
128
|
+
assert b1.state.name == 'HIDDEN'
|
|
129
|
+
assert b2.state.name == 'HIDDEN'
|
|
136
130
|
|
|
137
131
|
@staticmethod
|
|
138
|
-
def
|
|
139
|
-
"""Available update should
|
|
140
|
-
ctrl, _app, _client,
|
|
132
|
+
def test_update_available_shows_downloading_and_starts_download() -> None:
|
|
133
|
+
"""Available update should show downloading on all banners and start download."""
|
|
134
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
141
135
|
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
142
136
|
|
|
143
137
|
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
144
138
|
ctrl._on_check_finished(result, silent=False)
|
|
145
139
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
149
|
-
)
|
|
140
|
+
assert b1.state.name == 'DOWNLOADING'
|
|
141
|
+
assert b2.state.name == 'DOWNLOADING'
|
|
150
142
|
mock_dl.assert_called_once_with('2.0.0')
|
|
151
143
|
|
|
152
144
|
|
|
@@ -161,7 +153,7 @@ class TestDownloadFinished:
|
|
|
161
153
|
@staticmethod
|
|
162
154
|
def test_auto_apply_calls_apply_update() -> None:
|
|
163
155
|
"""When auto_apply=True, a successful download should call _apply_update(silent=True)."""
|
|
164
|
-
ctrl, app, client,
|
|
156
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=True)
|
|
165
157
|
|
|
166
158
|
with patch.object(ctrl, '_apply_update') as mock_apply:
|
|
167
159
|
ctrl._on_download_finished(True, '2.0.0')
|
|
@@ -171,45 +163,27 @@ class TestDownloadFinished:
|
|
|
171
163
|
@staticmethod
|
|
172
164
|
def test_auto_apply_does_not_show_ready_banner() -> None:
|
|
173
165
|
"""When auto_apply=True, the ready banner should NOT be shown."""
|
|
174
|
-
ctrl, app, client,
|
|
166
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=True)
|
|
175
167
|
|
|
176
168
|
with patch.object(ctrl, '_apply_update'):
|
|
177
169
|
ctrl._on_download_finished(True, '2.0.0')
|
|
178
170
|
|
|
179
|
-
|
|
180
|
-
assert
|
|
181
|
-
|
|
182
|
-
@staticmethod
|
|
183
|
-
def test_no_auto_apply_shows_ready_banner() -> None:
|
|
184
|
-
"""When auto_apply=False, a successful download should show the ready banner."""
|
|
185
|
-
ctrl, app, client, banner, settings = _make_controller(auto_apply=False)
|
|
186
|
-
ctrl._on_download_finished(True, '2.0.0')
|
|
187
|
-
|
|
188
|
-
assert banner.state.name == 'READY'
|
|
189
|
-
|
|
190
|
-
@staticmethod
|
|
191
|
-
def test_no_auto_apply_sets_ready_status() -> None:
|
|
192
|
-
"""When auto_apply=False, status should show 'v2.0.0 ready' in green."""
|
|
193
|
-
ctrl, app, client, banner, settings = _make_controller(auto_apply=False)
|
|
194
|
-
ctrl._on_download_finished(True, '2.0.0')
|
|
195
|
-
|
|
196
|
-
settings.set_update_status.assert_called_with(
|
|
197
|
-
'v2.0.0 ready',
|
|
198
|
-
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
199
|
-
)
|
|
171
|
+
assert b1.state.name != 'READY'
|
|
172
|
+
assert b2.state.name != 'READY'
|
|
200
173
|
|
|
201
174
|
@staticmethod
|
|
202
|
-
def
|
|
203
|
-
"""When auto_apply=False,
|
|
204
|
-
ctrl, app, client,
|
|
175
|
+
def test_no_auto_apply_shows_ready_banners() -> None:
|
|
176
|
+
"""When auto_apply=False, a successful download should show ready on all banners."""
|
|
177
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
205
178
|
ctrl._on_download_finished(True, '2.0.0')
|
|
206
179
|
|
|
207
|
-
|
|
180
|
+
assert b1.state.name == 'READY'
|
|
181
|
+
assert b2.state.name == 'READY'
|
|
208
182
|
|
|
209
183
|
@staticmethod
|
|
210
|
-
def
|
|
211
|
-
"""When user is active, the
|
|
212
|
-
ctrl, app, client,
|
|
184
|
+
def test_user_active_shows_ready_banners() -> None:
|
|
185
|
+
"""When user is active, the ready banners should be shown."""
|
|
186
|
+
ctrl, app, client, b1, b2, settings = _make_controller(
|
|
213
187
|
auto_apply=True,
|
|
214
188
|
is_user_active=True,
|
|
215
189
|
)
|
|
@@ -217,16 +191,92 @@ class TestDownloadFinished:
|
|
|
217
191
|
with patch.object(ctrl, '_apply_update'):
|
|
218
192
|
ctrl._on_download_finished(True, '2.0.0')
|
|
219
193
|
|
|
220
|
-
|
|
194
|
+
assert b1.state.name == 'READY'
|
|
195
|
+
assert b2.state.name == 'READY'
|
|
221
196
|
|
|
222
197
|
@staticmethod
|
|
223
198
|
def test_download_failure_shows_error() -> None:
|
|
224
|
-
"""A failed download should show an error
|
|
225
|
-
ctrl, app, client,
|
|
199
|
+
"""A failed download should show an error on all banners."""
|
|
200
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
226
201
|
ctrl._on_download_finished(False, '2.0.0')
|
|
227
202
|
|
|
228
|
-
assert
|
|
229
|
-
|
|
203
|
+
assert b1.state.name == 'ERROR'
|
|
204
|
+
assert b2.state.name == 'ERROR'
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def test_download_sets_pending_version() -> None:
|
|
208
|
+
"""A successful download should set _pending_version."""
|
|
209
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
210
|
+
ctrl._on_download_finished(True, '2.0.0')
|
|
211
|
+
|
|
212
|
+
assert ctrl._pending_version == '2.0.0'
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Pending version — skip redundant downloads
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestPendingVersion:
|
|
221
|
+
"""Verify behaviour when an update is already downloaded and pending."""
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def test_check_skips_download_when_version_already_pending() -> None:
|
|
225
|
+
"""Re-checking the same version should restore ready state, not re-download."""
|
|
226
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
227
|
+
ctrl._pending_version = '2.0.0'
|
|
228
|
+
|
|
229
|
+
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
230
|
+
|
|
231
|
+
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
232
|
+
ctrl._on_check_finished(result, silent=True)
|
|
233
|
+
|
|
234
|
+
mock_dl.assert_not_called()
|
|
235
|
+
assert b1.state.name == 'READY'
|
|
236
|
+
assert b2.state.name == 'READY'
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def test_check_downloads_when_newer_version() -> None:
|
|
240
|
+
"""A different version should trigger a fresh download."""
|
|
241
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
242
|
+
ctrl._pending_version = '1.5.0'
|
|
243
|
+
|
|
244
|
+
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
245
|
+
|
|
246
|
+
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
247
|
+
ctrl._on_check_finished(result, silent=True)
|
|
248
|
+
|
|
249
|
+
mock_dl.assert_called_once_with('2.0.0')
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def test_do_check_preserves_banner_when_pending() -> None:
|
|
253
|
+
"""set_checking should NOT be called when an update is already pending."""
|
|
254
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
255
|
+
ctrl._pending_version = '2.0.0'
|
|
256
|
+
|
|
257
|
+
with patch('asyncio.create_task'):
|
|
258
|
+
ctrl._do_check(silent=True)
|
|
259
|
+
|
|
260
|
+
settings.set_checking.assert_not_called()
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def test_do_check_shows_checking_when_no_pending() -> None:
|
|
264
|
+
"""set_checking should be called when there is no pending update."""
|
|
265
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
266
|
+
|
|
267
|
+
with patch('asyncio.create_task'):
|
|
268
|
+
ctrl._do_check(silent=True)
|
|
269
|
+
|
|
270
|
+
settings.set_checking.assert_called_once()
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def test_apply_clears_pending_version() -> None:
|
|
274
|
+
"""_apply_update should clear _pending_version."""
|
|
275
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
276
|
+
ctrl._pending_version = '2.0.0'
|
|
277
|
+
ctrl._apply_update()
|
|
278
|
+
|
|
279
|
+
assert ctrl._pending_version is None
|
|
230
280
|
|
|
231
281
|
|
|
232
282
|
# ---------------------------------------------------------------------------
|
|
@@ -244,7 +294,7 @@ class TestUserActiveGating:
|
|
|
244
294
|
@staticmethod
|
|
245
295
|
def test_auto_check_always_runs() -> None:
|
|
246
296
|
"""_on_auto_check should call _do_check even when user is active."""
|
|
247
|
-
ctrl, _app, _client,
|
|
297
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(is_user_active=True)
|
|
248
298
|
|
|
249
299
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
250
300
|
ctrl._on_auto_check()
|
|
@@ -254,7 +304,7 @@ class TestUserActiveGating:
|
|
|
254
304
|
@staticmethod
|
|
255
305
|
def test_manual_check_unaffected_by_active_user() -> None:
|
|
256
306
|
"""_on_manual_check should always call _do_check regardless of user activity."""
|
|
257
|
-
ctrl, _app, _client,
|
|
307
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(is_user_active=True)
|
|
258
308
|
|
|
259
309
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
260
310
|
ctrl._on_manual_check()
|
|
@@ -263,8 +313,8 @@ class TestUserActiveGating:
|
|
|
263
313
|
|
|
264
314
|
@staticmethod
|
|
265
315
|
def test_auto_apply_deferred_when_user_active() -> None:
|
|
266
|
-
"""When auto_apply=True but user is active, show READY
|
|
267
|
-
ctrl, app, client,
|
|
316
|
+
"""When auto_apply=True but user is active, show READY banners instead of applying."""
|
|
317
|
+
ctrl, app, client, b1, b2, settings = _make_controller(
|
|
268
318
|
auto_apply=True,
|
|
269
319
|
is_user_active=True,
|
|
270
320
|
)
|
|
@@ -273,12 +323,13 @@ class TestUserActiveGating:
|
|
|
273
323
|
ctrl._on_download_finished(True, '2.0.0')
|
|
274
324
|
|
|
275
325
|
mock_apply.assert_not_called()
|
|
276
|
-
assert
|
|
326
|
+
assert b1.state.name == 'READY'
|
|
327
|
+
assert b2.state.name == 'READY'
|
|
277
328
|
|
|
278
329
|
@staticmethod
|
|
279
330
|
def test_auto_apply_proceeds_when_user_inactive() -> None:
|
|
280
331
|
"""When auto_apply=True and user is inactive, _apply_update is called."""
|
|
281
|
-
ctrl, app, client,
|
|
332
|
+
ctrl, app, client, b1, b2, settings = _make_controller(
|
|
282
333
|
auto_apply=True,
|
|
283
334
|
is_user_active=False,
|
|
284
335
|
)
|
|
@@ -318,7 +369,7 @@ class TestApplyUpdate:
|
|
|
318
369
|
@staticmethod
|
|
319
370
|
def test_apply_update_calls_client_and_quits() -> None:
|
|
320
371
|
"""_apply_update should call client.apply_update_on_exit and app.quit."""
|
|
321
|
-
ctrl, app, client,
|
|
372
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
322
373
|
ctrl._apply_update()
|
|
323
374
|
|
|
324
375
|
client.apply_update_on_exit.assert_called_once_with(restart=True, silent=False)
|
|
@@ -327,21 +378,13 @@ class TestApplyUpdate:
|
|
|
327
378
|
@staticmethod
|
|
328
379
|
def test_apply_update_noop_without_updater() -> None:
|
|
329
380
|
"""_apply_update should be a no-op when client.updater is None."""
|
|
330
|
-
ctrl, app, client,
|
|
381
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
331
382
|
client.updater = None
|
|
332
383
|
ctrl._apply_update()
|
|
333
384
|
|
|
334
385
|
client.apply_update_on_exit.assert_not_called()
|
|
335
386
|
app.quit.assert_not_called()
|
|
336
387
|
|
|
337
|
-
@staticmethod
|
|
338
|
-
def test_restart_requested_signal_triggers_apply() -> None:
|
|
339
|
-
"""The settings restart_requested signal should be connected to _apply_update."""
|
|
340
|
-
ctrl, app, client, banner, settings = _make_controller()
|
|
341
|
-
|
|
342
|
-
# Verify the signal was connected
|
|
343
|
-
settings.restart_requested.connect.assert_called_once_with(ctrl._apply_update)
|
|
344
|
-
|
|
345
388
|
|
|
346
389
|
# ---------------------------------------------------------------------------
|
|
347
390
|
# Settings changed → immediate check
|
|
@@ -354,7 +397,7 @@ class TestSettingsChanged:
|
|
|
354
397
|
@staticmethod
|
|
355
398
|
def test_settings_changed_triggers_reinit_and_check() -> None:
|
|
356
399
|
"""Changing settings should reinitialise the updater and check."""
|
|
357
|
-
ctrl, app, client,
|
|
400
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
358
401
|
|
|
359
402
|
new_config = _make_config(update_channel='dev')
|
|
360
403
|
|
|
@@ -370,7 +413,7 @@ class TestSettingsChanged:
|
|
|
370
413
|
@staticmethod
|
|
371
414
|
def test_settings_changed_updates_auto_apply() -> None:
|
|
372
415
|
"""Changing settings should update the auto_apply flag."""
|
|
373
|
-
ctrl, app, client,
|
|
416
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=True)
|
|
374
417
|
|
|
375
418
|
new_config = _make_config(auto_apply=False)
|
|
376
419
|
|
|
@@ -391,26 +434,20 @@ class TestSettingsChanged:
|
|
|
391
434
|
class TestCheckError:
|
|
392
435
|
"""Verify _on_check_error routes errors correctly."""
|
|
393
436
|
|
|
394
|
-
@staticmethod
|
|
395
|
-
def test_check_error_sets_failed_status() -> None:
|
|
396
|
-
"""An exception during check should set 'Check failed' status."""
|
|
397
|
-
ctrl, app, client, banner, settings = _make_controller()
|
|
398
|
-
ctrl._on_check_error('connection refused', silent=False)
|
|
399
|
-
|
|
400
|
-
settings.set_update_status.assert_called_with('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
401
|
-
|
|
402
437
|
@staticmethod
|
|
403
438
|
def test_check_error_shows_banner_when_not_silent() -> None:
|
|
404
439
|
"""An exception during check should show banner when not silent."""
|
|
405
|
-
ctrl, app, client,
|
|
440
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
406
441
|
ctrl._on_check_error('timeout', silent=False)
|
|
407
442
|
|
|
408
|
-
assert
|
|
443
|
+
assert b1.state.name == 'ERROR'
|
|
444
|
+
assert b2.state.name == 'ERROR'
|
|
409
445
|
|
|
410
446
|
@staticmethod
|
|
411
447
|
def test_check_error_no_banner_when_silent() -> None:
|
|
412
448
|
"""An exception during check should NOT show banner when silent."""
|
|
413
|
-
ctrl, app, client,
|
|
449
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
414
450
|
ctrl._on_check_error('timeout', silent=True)
|
|
415
451
|
|
|
416
|
-
assert
|
|
452
|
+
assert b1.state.name == 'HIDDEN'
|
|
453
|
+
assert b2.state.name == 'HIDDEN'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/synodic_client/application/workers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_feedback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev53 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|