synodic-client 0.0.1.dev54__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.dev54 → synodic_client-0.0.1.dev55}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/schema.py +19 -1
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/settings.py +16 -30
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/tray.py +1 -2
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/update_controller.py +45 -53
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_settings.py +8 -16
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_controller.py +75 -113
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/README.md +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev54 → 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.dev54 → 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,13 +67,13 @@ 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
|
|
@@ -94,13 +88,14 @@ class UpdateController:
|
|
|
94
88
|
self._auto_update_timer: QTimer | None = None
|
|
95
89
|
self._restart_auto_update_timer()
|
|
96
90
|
|
|
97
|
-
# Wire banner signals
|
|
98
|
-
self.
|
|
99
|
-
|
|
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))
|
|
100
96
|
|
|
101
97
|
# Wire settings check-updates button
|
|
102
98
|
self._settings_window.check_updates_requested.connect(self._on_manual_check)
|
|
103
|
-
self._settings_window.restart_requested.connect(self._apply_update)
|
|
104
99
|
|
|
105
100
|
def set_user_active_predicate(self, predicate: Callable[[], bool]) -> None:
|
|
106
101
|
"""Set the predicate used to defer auto-apply when the user is active.
|
|
@@ -212,10 +207,11 @@ class UpdateController:
|
|
|
212
207
|
"""Run an update check."""
|
|
213
208
|
if self._client.updater is None:
|
|
214
209
|
if not silent:
|
|
215
|
-
|
|
210
|
+
for view in self._views:
|
|
211
|
+
view.show_error('Updater is not initialized.')
|
|
216
212
|
return
|
|
217
213
|
|
|
218
|
-
# Preserve the
|
|
214
|
+
# Preserve the banner state when an update is already pending
|
|
219
215
|
if self._pending_version is None:
|
|
220
216
|
self._settings_window.set_checking()
|
|
221
217
|
|
|
@@ -237,23 +233,22 @@ class UpdateController:
|
|
|
237
233
|
self._settings_window.reset_check_updates_button()
|
|
238
234
|
|
|
239
235
|
if result is None:
|
|
240
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
241
236
|
if not silent:
|
|
242
|
-
|
|
237
|
+
for view in self._views:
|
|
238
|
+
view.show_error('Failed to check for updates.')
|
|
243
239
|
else:
|
|
244
240
|
logger.warning('Automatic update check failed (no result)')
|
|
245
241
|
return
|
|
246
242
|
|
|
247
243
|
if result.error:
|
|
248
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
249
244
|
if not silent:
|
|
250
|
-
self.
|
|
245
|
+
for view in self._views:
|
|
246
|
+
view.show_error(result.error)
|
|
251
247
|
else:
|
|
252
248
|
logger.warning('Automatic update check failed: %s', result.error)
|
|
253
249
|
return
|
|
254
250
|
|
|
255
251
|
if not result.available:
|
|
256
|
-
self._settings_window.set_update_status('Up to date', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
257
252
|
if not silent:
|
|
258
253
|
logger.info('No updates available (current: %s)', result.current_version)
|
|
259
254
|
else:
|
|
@@ -268,20 +263,17 @@ class UpdateController:
|
|
|
268
263
|
return
|
|
269
264
|
|
|
270
265
|
# New update available — download it
|
|
271
|
-
self.
|
|
272
|
-
|
|
273
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
274
|
-
)
|
|
275
|
-
self._banner.show_downloading(version)
|
|
266
|
+
for view in self._views:
|
|
267
|
+
view.show_downloading(version)
|
|
276
268
|
self._start_download(version)
|
|
277
269
|
|
|
278
270
|
def _on_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
279
271
|
"""Handle unexpected exception during update check."""
|
|
280
272
|
self._settings_window.reset_check_updates_button()
|
|
281
|
-
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
282
273
|
|
|
283
274
|
if not silent:
|
|
284
|
-
self.
|
|
275
|
+
for view in self._views:
|
|
276
|
+
view.show_error(f'Update check error: {error}')
|
|
285
277
|
else:
|
|
286
278
|
logger.warning('Automatic update check error: %s', error)
|
|
287
279
|
|
|
@@ -298,50 +290,49 @@ class UpdateController:
|
|
|
298
290
|
try:
|
|
299
291
|
success = await download_update(
|
|
300
292
|
self._client,
|
|
301
|
-
on_progress=self.
|
|
293
|
+
on_progress=self._on_download_progress,
|
|
302
294
|
)
|
|
303
295
|
self._on_download_finished(success, version)
|
|
304
296
|
except Exception as exc:
|
|
305
297
|
logger.exception('Update download failed')
|
|
306
298
|
self._on_download_error(str(exc))
|
|
307
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
|
+
|
|
308
305
|
def _on_download_finished(self, success: bool, version: str) -> None:
|
|
309
306
|
"""Handle download completion."""
|
|
310
307
|
if not success:
|
|
311
|
-
|
|
312
|
-
|
|
308
|
+
for view in self._views:
|
|
309
|
+
view.show_error('Download failed. Please try again later.')
|
|
313
310
|
return
|
|
314
311
|
|
|
315
|
-
# Persist the client update timestamp
|
|
316
|
-
|
|
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)
|
|
317
316
|
|
|
318
317
|
self._pending_version = version
|
|
319
318
|
|
|
320
319
|
if self._can_auto_apply():
|
|
321
320
|
# Silently apply and restart — no banner, no user interaction
|
|
322
321
|
logger.info('Auto-applying update v%s', version)
|
|
323
|
-
self._settings_window.set_update_status(
|
|
324
|
-
f'v{version} installing\u2026',
|
|
325
|
-
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
326
|
-
)
|
|
327
322
|
self._apply_update(silent=True)
|
|
328
323
|
return
|
|
329
324
|
|
|
330
325
|
self._show_ready(version)
|
|
331
326
|
|
|
332
327
|
def _show_ready(self, version: str) -> None:
|
|
333
|
-
"""Present the *ready to restart* state
|
|
334
|
-
self.
|
|
335
|
-
|
|
336
|
-
f'v{version} ready',
|
|
337
|
-
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
338
|
-
)
|
|
339
|
-
self._settings_window.show_restart_button()
|
|
328
|
+
"""Present the *ready to restart* state across all views."""
|
|
329
|
+
for view in self._views:
|
|
330
|
+
view.show_ready(version)
|
|
340
331
|
|
|
341
332
|
def _on_download_error(self, error: str) -> None:
|
|
342
|
-
"""Handle download error — show error
|
|
343
|
-
self.
|
|
344
|
-
|
|
333
|
+
"""Handle download error — show error across all views."""
|
|
334
|
+
for view in self._views:
|
|
335
|
+
view.show_error(f'Download error: {error}')
|
|
345
336
|
|
|
346
337
|
# ------------------------------------------------------------------
|
|
347
338
|
# Apply
|
|
@@ -364,4 +355,5 @@ class UpdateController:
|
|
|
364
355
|
self._app.quit()
|
|
365
356
|
except Exception as e:
|
|
366
357
|
logger.error('Failed to apply update: %s', e)
|
|
367
|
-
|
|
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.dev54 → 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
|
|
171
|
+
assert b1.state.name != 'READY'
|
|
172
|
+
assert b2.state.name != 'READY'
|
|
181
173
|
|
|
182
174
|
@staticmethod
|
|
183
|
-
def
|
|
184
|
-
"""When auto_apply=False, a successful download should show
|
|
185
|
-
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)
|
|
186
178
|
ctrl._on_download_finished(True, '2.0.0')
|
|
187
179
|
|
|
188
|
-
assert
|
|
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
|
-
)
|
|
180
|
+
assert b1.state.name == 'READY'
|
|
181
|
+
assert b2.state.name == 'READY'
|
|
200
182
|
|
|
201
183
|
@staticmethod
|
|
202
|
-
def
|
|
203
|
-
"""When
|
|
204
|
-
ctrl, app, client,
|
|
205
|
-
ctrl._on_download_finished(True, '2.0.0')
|
|
206
|
-
|
|
207
|
-
settings.show_restart_button.assert_called_once()
|
|
208
|
-
|
|
209
|
-
@staticmethod
|
|
210
|
-
def test_user_active_shows_restart_button() -> None:
|
|
211
|
-
"""When user is active, the restart button should be shown in settings."""
|
|
212
|
-
ctrl, app, client, banner, settings = _make_controller(
|
|
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,21 +191,22 @@ 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'
|
|
230
205
|
|
|
231
206
|
@staticmethod
|
|
232
207
|
def test_download_sets_pending_version() -> None:
|
|
233
208
|
"""A successful download should set _pending_version."""
|
|
234
|
-
ctrl, app, client,
|
|
209
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
235
210
|
ctrl._on_download_finished(True, '2.0.0')
|
|
236
211
|
|
|
237
212
|
assert ctrl._pending_version == '2.0.0'
|
|
@@ -248,7 +223,7 @@ class TestPendingVersion:
|
|
|
248
223
|
@staticmethod
|
|
249
224
|
def test_check_skips_download_when_version_already_pending() -> None:
|
|
250
225
|
"""Re-checking the same version should restore ready state, not re-download."""
|
|
251
|
-
ctrl, _app, _client,
|
|
226
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
252
227
|
ctrl._pending_version = '2.0.0'
|
|
253
228
|
|
|
254
229
|
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
@@ -257,13 +232,13 @@ class TestPendingVersion:
|
|
|
257
232
|
ctrl._on_check_finished(result, silent=True)
|
|
258
233
|
|
|
259
234
|
mock_dl.assert_not_called()
|
|
260
|
-
assert
|
|
261
|
-
|
|
235
|
+
assert b1.state.name == 'READY'
|
|
236
|
+
assert b2.state.name == 'READY'
|
|
262
237
|
|
|
263
238
|
@staticmethod
|
|
264
239
|
def test_check_downloads_when_newer_version() -> None:
|
|
265
240
|
"""A different version should trigger a fresh download."""
|
|
266
|
-
ctrl, _app, _client,
|
|
241
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(auto_apply=False)
|
|
267
242
|
ctrl._pending_version = '1.5.0'
|
|
268
243
|
|
|
269
244
|
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
@@ -274,9 +249,9 @@ class TestPendingVersion:
|
|
|
274
249
|
mock_dl.assert_called_once_with('2.0.0')
|
|
275
250
|
|
|
276
251
|
@staticmethod
|
|
277
|
-
def
|
|
252
|
+
def test_do_check_preserves_banner_when_pending() -> None:
|
|
278
253
|
"""set_checking should NOT be called when an update is already pending."""
|
|
279
|
-
ctrl, _app, _client,
|
|
254
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
280
255
|
ctrl._pending_version = '2.0.0'
|
|
281
256
|
|
|
282
257
|
with patch('asyncio.create_task'):
|
|
@@ -287,7 +262,7 @@ class TestPendingVersion:
|
|
|
287
262
|
@staticmethod
|
|
288
263
|
def test_do_check_shows_checking_when_no_pending() -> None:
|
|
289
264
|
"""set_checking should be called when there is no pending update."""
|
|
290
|
-
ctrl, _app, _client,
|
|
265
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller()
|
|
291
266
|
|
|
292
267
|
with patch('asyncio.create_task'):
|
|
293
268
|
ctrl._do_check(silent=True)
|
|
@@ -297,7 +272,7 @@ class TestPendingVersion:
|
|
|
297
272
|
@staticmethod
|
|
298
273
|
def test_apply_clears_pending_version() -> None:
|
|
299
274
|
"""_apply_update should clear _pending_version."""
|
|
300
|
-
ctrl, app, client,
|
|
275
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
301
276
|
ctrl._pending_version = '2.0.0'
|
|
302
277
|
ctrl._apply_update()
|
|
303
278
|
|
|
@@ -319,7 +294,7 @@ class TestUserActiveGating:
|
|
|
319
294
|
@staticmethod
|
|
320
295
|
def test_auto_check_always_runs() -> None:
|
|
321
296
|
"""_on_auto_check should call _do_check even when user is active."""
|
|
322
|
-
ctrl, _app, _client,
|
|
297
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(is_user_active=True)
|
|
323
298
|
|
|
324
299
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
325
300
|
ctrl._on_auto_check()
|
|
@@ -329,7 +304,7 @@ class TestUserActiveGating:
|
|
|
329
304
|
@staticmethod
|
|
330
305
|
def test_manual_check_unaffected_by_active_user() -> None:
|
|
331
306
|
"""_on_manual_check should always call _do_check regardless of user activity."""
|
|
332
|
-
ctrl, _app, _client,
|
|
307
|
+
ctrl, _app, _client, b1, b2, settings = _make_controller(is_user_active=True)
|
|
333
308
|
|
|
334
309
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
335
310
|
ctrl._on_manual_check()
|
|
@@ -338,8 +313,8 @@ class TestUserActiveGating:
|
|
|
338
313
|
|
|
339
314
|
@staticmethod
|
|
340
315
|
def test_auto_apply_deferred_when_user_active() -> None:
|
|
341
|
-
"""When auto_apply=True but user is active, show READY
|
|
342
|
-
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(
|
|
343
318
|
auto_apply=True,
|
|
344
319
|
is_user_active=True,
|
|
345
320
|
)
|
|
@@ -348,12 +323,13 @@ class TestUserActiveGating:
|
|
|
348
323
|
ctrl._on_download_finished(True, '2.0.0')
|
|
349
324
|
|
|
350
325
|
mock_apply.assert_not_called()
|
|
351
|
-
assert
|
|
326
|
+
assert b1.state.name == 'READY'
|
|
327
|
+
assert b2.state.name == 'READY'
|
|
352
328
|
|
|
353
329
|
@staticmethod
|
|
354
330
|
def test_auto_apply_proceeds_when_user_inactive() -> None:
|
|
355
331
|
"""When auto_apply=True and user is inactive, _apply_update is called."""
|
|
356
|
-
ctrl, app, client,
|
|
332
|
+
ctrl, app, client, b1, b2, settings = _make_controller(
|
|
357
333
|
auto_apply=True,
|
|
358
334
|
is_user_active=False,
|
|
359
335
|
)
|
|
@@ -393,7 +369,7 @@ class TestApplyUpdate:
|
|
|
393
369
|
@staticmethod
|
|
394
370
|
def test_apply_update_calls_client_and_quits() -> None:
|
|
395
371
|
"""_apply_update should call client.apply_update_on_exit and app.quit."""
|
|
396
|
-
ctrl, app, client,
|
|
372
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
397
373
|
ctrl._apply_update()
|
|
398
374
|
|
|
399
375
|
client.apply_update_on_exit.assert_called_once_with(restart=True, silent=False)
|
|
@@ -402,21 +378,13 @@ class TestApplyUpdate:
|
|
|
402
378
|
@staticmethod
|
|
403
379
|
def test_apply_update_noop_without_updater() -> None:
|
|
404
380
|
"""_apply_update should be a no-op when client.updater is None."""
|
|
405
|
-
ctrl, app, client,
|
|
381
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
406
382
|
client.updater = None
|
|
407
383
|
ctrl._apply_update()
|
|
408
384
|
|
|
409
385
|
client.apply_update_on_exit.assert_not_called()
|
|
410
386
|
app.quit.assert_not_called()
|
|
411
387
|
|
|
412
|
-
@staticmethod
|
|
413
|
-
def test_restart_requested_signal_triggers_apply() -> None:
|
|
414
|
-
"""The settings restart_requested signal should be connected to _apply_update."""
|
|
415
|
-
ctrl, app, client, banner, settings = _make_controller()
|
|
416
|
-
|
|
417
|
-
# Verify the signal was connected
|
|
418
|
-
settings.restart_requested.connect.assert_called_once_with(ctrl._apply_update)
|
|
419
|
-
|
|
420
388
|
|
|
421
389
|
# ---------------------------------------------------------------------------
|
|
422
390
|
# Settings changed → immediate check
|
|
@@ -429,7 +397,7 @@ class TestSettingsChanged:
|
|
|
429
397
|
@staticmethod
|
|
430
398
|
def test_settings_changed_triggers_reinit_and_check() -> None:
|
|
431
399
|
"""Changing settings should reinitialise the updater and check."""
|
|
432
|
-
ctrl, app, client,
|
|
400
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
433
401
|
|
|
434
402
|
new_config = _make_config(update_channel='dev')
|
|
435
403
|
|
|
@@ -445,7 +413,7 @@ class TestSettingsChanged:
|
|
|
445
413
|
@staticmethod
|
|
446
414
|
def test_settings_changed_updates_auto_apply() -> None:
|
|
447
415
|
"""Changing settings should update the auto_apply flag."""
|
|
448
|
-
ctrl, app, client,
|
|
416
|
+
ctrl, app, client, b1, b2, settings = _make_controller(auto_apply=True)
|
|
449
417
|
|
|
450
418
|
new_config = _make_config(auto_apply=False)
|
|
451
419
|
|
|
@@ -466,26 +434,20 @@ class TestSettingsChanged:
|
|
|
466
434
|
class TestCheckError:
|
|
467
435
|
"""Verify _on_check_error routes errors correctly."""
|
|
468
436
|
|
|
469
|
-
@staticmethod
|
|
470
|
-
def test_check_error_sets_failed_status() -> None:
|
|
471
|
-
"""An exception during check should set 'Check failed' status."""
|
|
472
|
-
ctrl, app, client, banner, settings = _make_controller()
|
|
473
|
-
ctrl._on_check_error('connection refused', silent=False)
|
|
474
|
-
|
|
475
|
-
settings.set_update_status.assert_called_with('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
476
|
-
|
|
477
437
|
@staticmethod
|
|
478
438
|
def test_check_error_shows_banner_when_not_silent() -> None:
|
|
479
439
|
"""An exception during check should show banner when not silent."""
|
|
480
|
-
ctrl, app, client,
|
|
440
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
481
441
|
ctrl._on_check_error('timeout', silent=False)
|
|
482
442
|
|
|
483
|
-
assert
|
|
443
|
+
assert b1.state.name == 'ERROR'
|
|
444
|
+
assert b2.state.name == 'ERROR'
|
|
484
445
|
|
|
485
446
|
@staticmethod
|
|
486
447
|
def test_check_error_no_banner_when_silent() -> None:
|
|
487
448
|
"""An exception during check should NOT show banner when silent."""
|
|
488
|
-
ctrl, app, client,
|
|
449
|
+
ctrl, app, client, b1, b2, settings = _make_controller()
|
|
489
450
|
ctrl._on_check_error('timeout', silent=True)
|
|
490
451
|
|
|
491
|
-
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.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → 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.dev54 → 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.dev54 → synodic_client-0.0.1.dev55}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev54 → 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.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → 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.dev54 → 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.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → 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.dev54 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev54 → synodic_client-0.0.1.dev55}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|