synodic-client 0.0.1.dev55__tar.gz → 0.0.1.dev56__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.dev55 → synodic_client-0.0.1.dev56}/PKG-INFO +1 -1
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/pyproject.toml +1 -1
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/schema.py +19 -5
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/settings.py +36 -16
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/tray.py +2 -1
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/update_controller.py +38 -19
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_settings.py +16 -8
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_controller.py +104 -141
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/README.md +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_startup.py +0 -0
|
@@ -351,11 +351,25 @@ class UpdateView(Protocol):
|
|
|
351
351
|
status stays in sync.
|
|
352
352
|
"""
|
|
353
353
|
|
|
354
|
-
def show_downloading(self, version: str) -> None:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def
|
|
354
|
+
def show_downloading(self, version: str) -> None:
|
|
355
|
+
"""Indicate that *version* is being downloaded."""
|
|
356
|
+
...
|
|
357
|
+
|
|
358
|
+
def show_downloading_progress(self, percentage: int) -> None:
|
|
359
|
+
"""Update the download progress indicator."""
|
|
360
|
+
...
|
|
361
|
+
|
|
362
|
+
def show_ready(self, version: str) -> None:
|
|
363
|
+
"""Indicate that *version* is downloaded and ready to install."""
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
def show_error(self, message: str) -> None:
|
|
367
|
+
"""Display an error *message* in the update area."""
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
def hide_banner(self) -> None:
|
|
371
|
+
"""Hide the update banner."""
|
|
372
|
+
...
|
|
359
373
|
|
|
360
374
|
|
|
361
375
|
class UpdateBannerState(Enum):
|
|
@@ -31,8 +31,7 @@ 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.
|
|
35
|
-
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
34
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE, UPDATE_STATUS_CHECKING_STYLE
|
|
36
35
|
from synodic_client.logging import log_path, set_debug_level
|
|
37
36
|
from synodic_client.resolution import ResolvedConfig, update_user_config
|
|
38
37
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
@@ -55,6 +54,9 @@ class SettingsWindow(QMainWindow):
|
|
|
55
54
|
check_updates_requested = Signal()
|
|
56
55
|
"""Emitted when the user clicks the *Check for Updates* button."""
|
|
57
56
|
|
|
57
|
+
restart_requested = Signal()
|
|
58
|
+
"""Emitted when the user clicks the *Restart & Update* button."""
|
|
59
|
+
|
|
58
60
|
def showEvent(self, event: QShowEvent) -> None: # noqa: N802
|
|
59
61
|
"""[DIAG] Log every show event with a stack trace."""
|
|
60
62
|
geo = self.geometry()
|
|
@@ -197,13 +199,18 @@ class SettingsWindow(QMainWindow):
|
|
|
197
199
|
self._check_updates_btn = QPushButton('Check for Updates\u2026')
|
|
198
200
|
self._check_updates_btn.clicked.connect(self._on_check_updates_clicked)
|
|
199
201
|
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
|
+
|
|
200
211
|
row.addStretch()
|
|
201
212
|
content.addLayout(row)
|
|
202
213
|
|
|
203
|
-
# Embedded update banner (same widget used in the main window)
|
|
204
|
-
self._update_banner = UpdateBanner()
|
|
205
|
-
content.addWidget(self._update_banner)
|
|
206
|
-
|
|
207
214
|
# Last client update timestamp
|
|
208
215
|
self._last_client_update_label = QLabel('')
|
|
209
216
|
self._last_client_update_label.setStyleSheet('color: #808080; font-size: 11px;')
|
|
@@ -273,25 +280,37 @@ class SettingsWindow(QMainWindow):
|
|
|
273
280
|
else:
|
|
274
281
|
self._last_client_update_label.setText('')
|
|
275
282
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
"""The embedded :class:`UpdateBanner` for this window."""
|
|
279
|
-
return self._update_banner
|
|
283
|
+
def set_update_status(self, text: str, style: str = '') -> None:
|
|
284
|
+
"""Set the inline status text next to the *Check for Updates* button.
|
|
280
285
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
self.
|
|
286
|
+
Args:
|
|
287
|
+
text: The status message.
|
|
288
|
+
style: Optional stylesheet for the label (e.g. color).
|
|
289
|
+
"""
|
|
290
|
+
self._update_status_label.setText(text)
|
|
291
|
+
self._update_status_label.setStyleSheet(style)
|
|
286
292
|
|
|
287
293
|
def set_checking(self) -> None:
|
|
288
|
-
"""Enter the *checking* state — disable button."""
|
|
294
|
+
"""Enter the *checking* state — disable button and show status."""
|
|
289
295
|
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)
|
|
290
299
|
|
|
291
300
|
def reset_check_updates_button(self) -> None:
|
|
292
301
|
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
293
302
|
self._check_updates_btn.setEnabled(True)
|
|
294
303
|
|
|
304
|
+
def set_last_checked(self, timestamp: str) -> None:
|
|
305
|
+
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
306
|
+
relative = _format_relative_time(timestamp)
|
|
307
|
+
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
308
|
+
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
309
|
+
|
|
310
|
+
def show_restart_button(self) -> None:
|
|
311
|
+
"""Show the *Restart & Update* button."""
|
|
312
|
+
self._restart_btn.show()
|
|
313
|
+
|
|
295
314
|
def show(self) -> None:
|
|
296
315
|
"""Sync controls from config, then show the window."""
|
|
297
316
|
self.sync_from_config()
|
|
@@ -337,6 +356,7 @@ class SettingsWindow(QMainWindow):
|
|
|
337
356
|
def _on_check_updates_clicked(self) -> None:
|
|
338
357
|
"""Handle the *Check for Updates* button click."""
|
|
339
358
|
self._check_updates_btn.setEnabled(False)
|
|
359
|
+
self._update_status_label.setText('Checking\u2026')
|
|
340
360
|
self.check_updates_requested.emit()
|
|
341
361
|
|
|
342
362
|
def _on_channel_changed(self, index: int) -> None:
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -68,10 +68,11 @@ 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
|
|
71
72
|
self._update_controller = UpdateController(
|
|
72
73
|
app,
|
|
73
74
|
client,
|
|
74
|
-
[
|
|
75
|
+
[self._banner],
|
|
75
76
|
settings_window=self._settings_window,
|
|
76
77
|
config=config,
|
|
77
78
|
)
|
|
@@ -19,6 +19,11 @@ from PySide6.QtWidgets import QApplication
|
|
|
19
19
|
|
|
20
20
|
from synodic_client.application.screen.schema import UpdateView
|
|
21
21
|
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
22
|
+
from synodic_client.application.theme import (
|
|
23
|
+
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
24
|
+
UPDATE_STATUS_ERROR_STYLE,
|
|
25
|
+
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
26
|
+
)
|
|
22
27
|
from synodic_client.application.workers import check_for_update, download_update
|
|
23
28
|
from synodic_client.resolution import (
|
|
24
29
|
ResolvedConfig,
|
|
@@ -94,8 +99,9 @@ class UpdateController:
|
|
|
94
99
|
view.restart_requested.connect(self._apply_update)
|
|
95
100
|
view.retry_requested.connect(lambda: self.check_now(silent=True))
|
|
96
101
|
|
|
97
|
-
# Wire settings check-updates
|
|
102
|
+
# Wire settings check-updates and restart buttons
|
|
98
103
|
self._settings_window.check_updates_requested.connect(self._on_manual_check)
|
|
104
|
+
self._settings_window.restart_requested.connect(self._apply_update)
|
|
99
105
|
|
|
100
106
|
def set_user_active_predicate(self, predicate: Callable[[], bool]) -> None:
|
|
101
107
|
"""Set the predicate used to defer auto-apply when the user is active.
|
|
@@ -123,6 +129,25 @@ class UpdateController:
|
|
|
123
129
|
"""
|
|
124
130
|
return self._auto_apply and not self._is_user_active()
|
|
125
131
|
|
|
132
|
+
def _persist_check_timestamp(self) -> None:
|
|
133
|
+
"""Persist the current time as *last_client_update* and refresh the label."""
|
|
134
|
+
ts = datetime.now(UTC).isoformat()
|
|
135
|
+
update_user_config(last_client_update=ts)
|
|
136
|
+
self._settings_window.set_last_checked(ts)
|
|
137
|
+
|
|
138
|
+
def _report_error(self, message: str, *, silent: bool) -> None:
|
|
139
|
+
"""Show an error to the user or log it, depending on *silent*.
|
|
140
|
+
|
|
141
|
+
Always updates the settings status line. When not *silent*,
|
|
142
|
+
also broadcasts the error to all update-banner views.
|
|
143
|
+
"""
|
|
144
|
+
self._settings_window.set_update_status('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
145
|
+
if silent:
|
|
146
|
+
logger.warning('%s', message)
|
|
147
|
+
else:
|
|
148
|
+
for view in self._views:
|
|
149
|
+
view.show_error(message)
|
|
150
|
+
|
|
126
151
|
# ------------------------------------------------------------------
|
|
127
152
|
# Timer management
|
|
128
153
|
# ------------------------------------------------------------------
|
|
@@ -233,22 +258,18 @@ class UpdateController:
|
|
|
233
258
|
self._settings_window.reset_check_updates_button()
|
|
234
259
|
|
|
235
260
|
if result is None:
|
|
236
|
-
|
|
237
|
-
for view in self._views:
|
|
238
|
-
view.show_error('Failed to check for updates.')
|
|
239
|
-
else:
|
|
240
|
-
logger.warning('Automatic update check failed (no result)')
|
|
261
|
+
self._report_error('Failed to check for updates.', silent=silent)
|
|
241
262
|
return
|
|
242
263
|
|
|
243
264
|
if result.error:
|
|
244
|
-
|
|
245
|
-
for view in self._views:
|
|
246
|
-
view.show_error(result.error)
|
|
247
|
-
else:
|
|
248
|
-
logger.warning('Automatic update check failed: %s', result.error)
|
|
265
|
+
self._report_error(result.error, silent=silent)
|
|
249
266
|
return
|
|
250
267
|
|
|
268
|
+
# Successful check — refresh the "last updated" timestamp
|
|
269
|
+
self._persist_check_timestamp()
|
|
270
|
+
|
|
251
271
|
if not result.available:
|
|
272
|
+
self._settings_window.set_update_status('Up to date', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
252
273
|
if not silent:
|
|
253
274
|
logger.info('No updates available (current: %s)', result.current_version)
|
|
254
275
|
else:
|
|
@@ -263,6 +284,7 @@ class UpdateController:
|
|
|
263
284
|
return
|
|
264
285
|
|
|
265
286
|
# New update available — download it
|
|
287
|
+
self._settings_window.set_update_status(f'v{version} available', UPDATE_STATUS_AVAILABLE_STYLE)
|
|
266
288
|
for view in self._views:
|
|
267
289
|
view.show_downloading(version)
|
|
268
290
|
self._start_download(version)
|
|
@@ -270,12 +292,7 @@ class UpdateController:
|
|
|
270
292
|
def _on_check_error(self, error: str, *, silent: bool = False) -> None:
|
|
271
293
|
"""Handle unexpected exception during update check."""
|
|
272
294
|
self._settings_window.reset_check_updates_button()
|
|
273
|
-
|
|
274
|
-
if not silent:
|
|
275
|
-
for view in self._views:
|
|
276
|
-
view.show_error(f'Update check error: {error}')
|
|
277
|
-
else:
|
|
278
|
-
logger.warning('Automatic update check error: %s', error)
|
|
295
|
+
self._report_error(f'Update check error: {error}', silent=silent)
|
|
279
296
|
|
|
280
297
|
# ------------------------------------------------------------------
|
|
281
298
|
# Download flow
|
|
@@ -305,14 +322,14 @@ class UpdateController:
|
|
|
305
322
|
def _on_download_finished(self, success: bool, version: str) -> None:
|
|
306
323
|
"""Handle download completion."""
|
|
307
324
|
if not success:
|
|
325
|
+
self._settings_window.set_update_status('Download failed', UPDATE_STATUS_ERROR_STYLE)
|
|
308
326
|
for view in self._views:
|
|
309
327
|
view.show_error('Download failed. Please try again later.')
|
|
310
328
|
return
|
|
311
329
|
|
|
312
|
-
# Persist
|
|
330
|
+
# Persist the client-update timestamp (actual update downloaded)
|
|
313
331
|
ts = datetime.now(UTC).isoformat()
|
|
314
332
|
update_user_config(last_client_update=ts)
|
|
315
|
-
self._settings_window.set_last_updated(ts)
|
|
316
333
|
|
|
317
334
|
self._pending_version = version
|
|
318
335
|
|
|
@@ -326,6 +343,8 @@ class UpdateController:
|
|
|
326
343
|
|
|
327
344
|
def _show_ready(self, version: str) -> None:
|
|
328
345
|
"""Present the *ready to restart* state across all views."""
|
|
346
|
+
self._settings_window.set_update_status(f'v{version} ready', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
347
|
+
self._settings_window.show_restart_button()
|
|
329
348
|
for view in self._views:
|
|
330
349
|
view.show_ready(version)
|
|
331
350
|
|
|
@@ -334,18 +334,16 @@ 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 inline status label."""
|
|
338
338
|
|
|
339
339
|
@staticmethod
|
|
340
|
-
def
|
|
341
|
-
"""Window has the check-updates button and
|
|
340
|
+
def test_button_and_label_exist() -> None:
|
|
341
|
+
"""Window has the check-updates button and status label."""
|
|
342
342
|
window = _make_window()
|
|
343
343
|
assert hasattr(window, '_check_updates_btn')
|
|
344
|
+
assert hasattr(window, '_update_status_label')
|
|
344
345
|
assert window._check_updates_btn.text() == 'Check for Updates\u2026'
|
|
345
|
-
assert
|
|
346
|
-
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
347
|
-
|
|
348
|
-
assert isinstance(window.update_banner, UpdateBanner)
|
|
346
|
+
assert not window._update_status_label.text()
|
|
349
347
|
|
|
350
348
|
@staticmethod
|
|
351
349
|
def test_click_emits_signal_and_disables() -> None:
|
|
@@ -358,6 +356,15 @@ class TestCheckForUpdatesButton:
|
|
|
358
356
|
|
|
359
357
|
signal_spy.assert_called_once()
|
|
360
358
|
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()
|
|
361
368
|
|
|
362
369
|
@staticmethod
|
|
363
370
|
def test_reset_check_updates_button() -> None:
|
|
@@ -371,7 +378,8 @@ class TestCheckForUpdatesButton:
|
|
|
371
378
|
|
|
372
379
|
@staticmethod
|
|
373
380
|
def test_set_checking() -> None:
|
|
374
|
-
"""set_checking disables the button."""
|
|
381
|
+
"""set_checking disables the button and shows 'Checking\u2026' status."""
|
|
375
382
|
window = _make_window()
|
|
376
383
|
window.set_checking()
|
|
377
384
|
assert window._check_updates_btn.isEnabled() is False
|
|
385
|
+
assert window._update_status_label.text() == 'Checking\u2026'
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -8,6 +8,11 @@ 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
|
+
)
|
|
11
16
|
from synodic_client.application.update_controller import UpdateController
|
|
12
17
|
from synodic_client.resolution import ResolvedConfig
|
|
13
18
|
from synodic_client.schema import (
|
|
@@ -46,10 +51,10 @@ def _make_controller(
|
|
|
46
51
|
auto_apply: bool = True,
|
|
47
52
|
auto_update_interval_minutes: int = 0,
|
|
48
53
|
is_user_active: bool = False,
|
|
49
|
-
) -> tuple[UpdateController, MagicMock, MagicMock, UpdateBanner,
|
|
54
|
+
) -> tuple[UpdateController, MagicMock, MagicMock, UpdateBanner, MagicMock]:
|
|
50
55
|
"""Build an ``UpdateController`` with mocked collaborators.
|
|
51
56
|
|
|
52
|
-
Returns (controller, app_mock, client_mock,
|
|
57
|
+
Returns (controller, app_mock, client_mock, banner, settings_mock).
|
|
53
58
|
"""
|
|
54
59
|
config = _make_config(
|
|
55
60
|
auto_apply=auto_apply,
|
|
@@ -59,11 +64,8 @@ def _make_controller(
|
|
|
59
64
|
app = MagicMock()
|
|
60
65
|
client = MagicMock()
|
|
61
66
|
client.updater = MagicMock()
|
|
62
|
-
|
|
63
|
-
banner2 = UpdateBanner()
|
|
67
|
+
banner = UpdateBanner()
|
|
64
68
|
settings = MagicMock()
|
|
65
|
-
# settings mock only needs: check_updates_requested, set_checking,
|
|
66
|
-
# reset_check_updates_button, set_last_updated
|
|
67
69
|
|
|
68
70
|
with patch('synodic_client.application.update_controller.resolve_update_config') as mock_ucfg:
|
|
69
71
|
mock_ucfg.return_value = MagicMock(
|
|
@@ -72,13 +74,13 @@ def _make_controller(
|
|
|
72
74
|
controller = UpdateController(
|
|
73
75
|
app,
|
|
74
76
|
client,
|
|
75
|
-
[
|
|
77
|
+
[banner],
|
|
76
78
|
settings_window=settings,
|
|
77
79
|
config=config,
|
|
78
80
|
)
|
|
79
81
|
controller.set_user_active_predicate(lambda: is_user_active)
|
|
80
82
|
|
|
81
|
-
return controller, app, client,
|
|
83
|
+
return controller, app, client, banner, settings
|
|
82
84
|
|
|
83
85
|
|
|
84
86
|
# ---------------------------------------------------------------------------
|
|
@@ -90,55 +92,61 @@ class TestCheckFinished:
|
|
|
90
92
|
"""Verify _on_check_finished routes results correctly."""
|
|
91
93
|
|
|
92
94
|
@staticmethod
|
|
93
|
-
def
|
|
94
|
-
"""A None result
|
|
95
|
-
ctrl, _app, _client,
|
|
95
|
+
def test_none_result_sets_error_status() -> None:
|
|
96
|
+
"""A None result should set 'Check failed' in red."""
|
|
97
|
+
ctrl, _app, _client, banner, settings = _make_controller()
|
|
96
98
|
ctrl._on_check_finished(None, silent=False)
|
|
97
99
|
|
|
98
100
|
settings.reset_check_updates_button.assert_called_once()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
settings.set_update_status.assert_called_once_with('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
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'
|
|
101
110
|
|
|
102
111
|
@staticmethod
|
|
103
112
|
def test_none_result_no_banner_when_silent() -> None:
|
|
104
113
|
"""A None result with silent=True should NOT show the error banner."""
|
|
105
|
-
ctrl, _app, _client,
|
|
114
|
+
ctrl, _app, _client, banner, settings = _make_controller()
|
|
106
115
|
ctrl._on_check_finished(None, silent=True)
|
|
107
116
|
|
|
108
|
-
assert
|
|
109
|
-
assert b2.state.name == 'HIDDEN'
|
|
117
|
+
assert banner.state.name == 'HIDDEN'
|
|
110
118
|
|
|
111
119
|
@staticmethod
|
|
112
|
-
def
|
|
113
|
-
"""An error result should
|
|
114
|
-
ctrl, _app, _client,
|
|
120
|
+
def test_error_result_sets_error_status() -> None:
|
|
121
|
+
"""An error result should set 'Check failed' status."""
|
|
122
|
+
ctrl, _app, _client, banner, settings = _make_controller()
|
|
115
123
|
result = UpdateInfo(available=False, current_version=Version('1.0.0'), error='No releases found')
|
|
116
124
|
ctrl._on_check_finished(result, silent=False)
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
assert b2.state.name == 'ERROR'
|
|
126
|
+
settings.set_update_status.assert_called_once_with('Check failed', UPDATE_STATUS_ERROR_STYLE)
|
|
120
127
|
|
|
121
128
|
@staticmethod
|
|
122
|
-
def
|
|
123
|
-
"""No update available should
|
|
124
|
-
ctrl, _app, _client,
|
|
129
|
+
def test_no_update_sets_up_to_date() -> None:
|
|
130
|
+
"""No update available should set 'Up to date' in green."""
|
|
131
|
+
ctrl, _app, _client, banner, settings = _make_controller()
|
|
125
132
|
result = UpdateInfo(available=False, current_version=Version('1.0.0'))
|
|
126
133
|
ctrl._on_check_finished(result, silent=False)
|
|
127
134
|
|
|
128
|
-
|
|
129
|
-
assert b2.state.name == 'HIDDEN'
|
|
135
|
+
settings.set_update_status.assert_called_once_with('Up to date', UPDATE_STATUS_UP_TO_DATE_STYLE)
|
|
130
136
|
|
|
131
137
|
@staticmethod
|
|
132
|
-
def
|
|
133
|
-
"""Available update should
|
|
134
|
-
ctrl, _app, _client,
|
|
138
|
+
def test_update_available_sets_status_and_starts_download() -> None:
|
|
139
|
+
"""Available update should set orange status and start download."""
|
|
140
|
+
ctrl, _app, _client, banner, settings = _make_controller()
|
|
135
141
|
result = UpdateInfo(available=True, current_version=Version('1.0.0'), latest_version=Version('2.0.0'))
|
|
136
142
|
|
|
137
143
|
with patch.object(ctrl, '_start_download') as mock_dl:
|
|
138
144
|
ctrl._on_check_finished(result, silent=False)
|
|
139
145
|
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
settings.set_update_status.assert_called_once_with(
|
|
147
|
+
'v2.0.0 available',
|
|
148
|
+
UPDATE_STATUS_AVAILABLE_STYLE,
|
|
149
|
+
)
|
|
142
150
|
mock_dl.assert_called_once_with('2.0.0')
|
|
143
151
|
|
|
144
152
|
|
|
@@ -153,7 +161,7 @@ class TestDownloadFinished:
|
|
|
153
161
|
@staticmethod
|
|
154
162
|
def test_auto_apply_calls_apply_update() -> None:
|
|
155
163
|
"""When auto_apply=True, a successful download should call _apply_update(silent=True)."""
|
|
156
|
-
ctrl, app, client,
|
|
164
|
+
ctrl, app, client, banner, settings = _make_controller(auto_apply=True)
|
|
157
165
|
|
|
158
166
|
with patch.object(ctrl, '_apply_update') as mock_apply:
|
|
159
167
|
ctrl._on_download_finished(True, '2.0.0')
|
|
@@ -163,120 +171,62 @@ class TestDownloadFinished:
|
|
|
163
171
|
@staticmethod
|
|
164
172
|
def test_auto_apply_does_not_show_ready_banner() -> None:
|
|
165
173
|
"""When auto_apply=True, the ready banner should NOT be shown."""
|
|
166
|
-
ctrl, app, client,
|
|
174
|
+
ctrl, app, client, banner, settings = _make_controller(auto_apply=True)
|
|
167
175
|
|
|
168
176
|
with patch.object(ctrl, '_apply_update'):
|
|
169
177
|
ctrl._on_download_finished(True, '2.0.0')
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
assert
|
|
179
|
+
# Banner should not be in READY state
|
|
180
|
+
assert banner.state.name != 'READY'
|
|
173
181
|
|
|
174
182
|
@staticmethod
|
|
175
|
-
def
|
|
176
|
-
"""When auto_apply=False, a successful download should show ready
|
|
177
|
-
ctrl, app, client,
|
|
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)
|
|
178
186
|
ctrl._on_download_finished(True, '2.0.0')
|
|
179
187
|
|
|
180
|
-
assert
|
|
181
|
-
assert b2.state.name == 'READY'
|
|
182
|
-
|
|
183
|
-
@staticmethod
|
|
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(
|
|
187
|
-
auto_apply=True,
|
|
188
|
-
is_user_active=True,
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
with patch.object(ctrl, '_apply_update'):
|
|
192
|
-
ctrl._on_download_finished(True, '2.0.0')
|
|
193
|
-
|
|
194
|
-
assert b1.state.name == 'READY'
|
|
195
|
-
assert b2.state.name == 'READY'
|
|
188
|
+
assert banner.state.name == 'READY'
|
|
196
189
|
|
|
197
190
|
@staticmethod
|
|
198
|
-
def
|
|
199
|
-
"""
|
|
200
|
-
ctrl, app, client,
|
|
201
|
-
ctrl._on_download_finished(False, '2.0.0')
|
|
202
|
-
|
|
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)
|
|
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)
|
|
210
194
|
ctrl._on_download_finished(True, '2.0.0')
|
|
211
195
|
|
|
212
|
-
|
|
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')
|
|
196
|
+
settings.set_update_status.assert_called_with(
|
|
197
|
+
'v2.0.0 ready',
|
|
198
|
+
UPDATE_STATUS_UP_TO_DATE_STYLE,
|
|
199
|
+
)
|
|
250
200
|
|
|
251
201
|
@staticmethod
|
|
252
|
-
def
|
|
253
|
-
"""
|
|
254
|
-
ctrl,
|
|
255
|
-
ctrl.
|
|
256
|
-
|
|
257
|
-
with patch('asyncio.create_task'):
|
|
258
|
-
ctrl._do_check(silent=True)
|
|
202
|
+
def test_no_auto_apply_shows_restart_button() -> None:
|
|
203
|
+
"""When auto_apply=False, the restart button should be shown in settings."""
|
|
204
|
+
ctrl, app, client, banner, settings = _make_controller(auto_apply=False)
|
|
205
|
+
ctrl._on_download_finished(True, '2.0.0')
|
|
259
206
|
|
|
260
|
-
settings.
|
|
207
|
+
settings.show_restart_button.assert_called_once()
|
|
261
208
|
|
|
262
209
|
@staticmethod
|
|
263
|
-
def
|
|
264
|
-
"""
|
|
265
|
-
ctrl,
|
|
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(
|
|
213
|
+
auto_apply=True,
|
|
214
|
+
is_user_active=True,
|
|
215
|
+
)
|
|
266
216
|
|
|
267
|
-
with patch('
|
|
268
|
-
ctrl.
|
|
217
|
+
with patch.object(ctrl, '_apply_update'):
|
|
218
|
+
ctrl._on_download_finished(True, '2.0.0')
|
|
269
219
|
|
|
270
|
-
settings.
|
|
220
|
+
settings.show_restart_button.assert_called_once()
|
|
271
221
|
|
|
272
222
|
@staticmethod
|
|
273
|
-
def
|
|
274
|
-
"""
|
|
275
|
-
ctrl, app, client,
|
|
276
|
-
ctrl.
|
|
277
|
-
ctrl._apply_update()
|
|
223
|
+
def test_download_failure_shows_error() -> None:
|
|
224
|
+
"""A failed download should show an error banner."""
|
|
225
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
226
|
+
ctrl._on_download_finished(False, '2.0.0')
|
|
278
227
|
|
|
279
|
-
assert
|
|
228
|
+
assert banner.state.name == 'ERROR'
|
|
229
|
+
settings.set_update_status.assert_called_with('Download failed', UPDATE_STATUS_ERROR_STYLE)
|
|
280
230
|
|
|
281
231
|
|
|
282
232
|
# ---------------------------------------------------------------------------
|
|
@@ -294,7 +244,7 @@ class TestUserActiveGating:
|
|
|
294
244
|
@staticmethod
|
|
295
245
|
def test_auto_check_always_runs() -> None:
|
|
296
246
|
"""_on_auto_check should call _do_check even when user is active."""
|
|
297
|
-
ctrl, _app, _client,
|
|
247
|
+
ctrl, _app, _client, banner, settings = _make_controller(is_user_active=True)
|
|
298
248
|
|
|
299
249
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
300
250
|
ctrl._on_auto_check()
|
|
@@ -304,7 +254,7 @@ class TestUserActiveGating:
|
|
|
304
254
|
@staticmethod
|
|
305
255
|
def test_manual_check_unaffected_by_active_user() -> None:
|
|
306
256
|
"""_on_manual_check should always call _do_check regardless of user activity."""
|
|
307
|
-
ctrl, _app, _client,
|
|
257
|
+
ctrl, _app, _client, banner, settings = _make_controller(is_user_active=True)
|
|
308
258
|
|
|
309
259
|
with patch.object(ctrl, '_do_check') as mock_check:
|
|
310
260
|
ctrl._on_manual_check()
|
|
@@ -313,8 +263,8 @@ class TestUserActiveGating:
|
|
|
313
263
|
|
|
314
264
|
@staticmethod
|
|
315
265
|
def test_auto_apply_deferred_when_user_active() -> None:
|
|
316
|
-
"""When auto_apply=True but user is active, show READY
|
|
317
|
-
ctrl, app, client,
|
|
266
|
+
"""When auto_apply=True but user is active, show READY banner instead of applying."""
|
|
267
|
+
ctrl, app, client, banner, settings = _make_controller(
|
|
318
268
|
auto_apply=True,
|
|
319
269
|
is_user_active=True,
|
|
320
270
|
)
|
|
@@ -323,13 +273,12 @@ class TestUserActiveGating:
|
|
|
323
273
|
ctrl._on_download_finished(True, '2.0.0')
|
|
324
274
|
|
|
325
275
|
mock_apply.assert_not_called()
|
|
326
|
-
assert
|
|
327
|
-
assert b2.state.name == 'READY'
|
|
276
|
+
assert banner.state.name == 'READY'
|
|
328
277
|
|
|
329
278
|
@staticmethod
|
|
330
279
|
def test_auto_apply_proceeds_when_user_inactive() -> None:
|
|
331
280
|
"""When auto_apply=True and user is inactive, _apply_update is called."""
|
|
332
|
-
ctrl, app, client,
|
|
281
|
+
ctrl, app, client, banner, settings = _make_controller(
|
|
333
282
|
auto_apply=True,
|
|
334
283
|
is_user_active=False,
|
|
335
284
|
)
|
|
@@ -369,7 +318,7 @@ class TestApplyUpdate:
|
|
|
369
318
|
@staticmethod
|
|
370
319
|
def test_apply_update_calls_client_and_quits() -> None:
|
|
371
320
|
"""_apply_update should call client.apply_update_on_exit and app.quit."""
|
|
372
|
-
ctrl, app, client,
|
|
321
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
373
322
|
ctrl._apply_update()
|
|
374
323
|
|
|
375
324
|
client.apply_update_on_exit.assert_called_once_with(restart=True, silent=False)
|
|
@@ -378,13 +327,21 @@ class TestApplyUpdate:
|
|
|
378
327
|
@staticmethod
|
|
379
328
|
def test_apply_update_noop_without_updater() -> None:
|
|
380
329
|
"""_apply_update should be a no-op when client.updater is None."""
|
|
381
|
-
ctrl, app, client,
|
|
330
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
382
331
|
client.updater = None
|
|
383
332
|
ctrl._apply_update()
|
|
384
333
|
|
|
385
334
|
client.apply_update_on_exit.assert_not_called()
|
|
386
335
|
app.quit.assert_not_called()
|
|
387
336
|
|
|
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
|
+
|
|
388
345
|
|
|
389
346
|
# ---------------------------------------------------------------------------
|
|
390
347
|
# Settings changed → immediate check
|
|
@@ -397,7 +354,7 @@ class TestSettingsChanged:
|
|
|
397
354
|
@staticmethod
|
|
398
355
|
def test_settings_changed_triggers_reinit_and_check() -> None:
|
|
399
356
|
"""Changing settings should reinitialise the updater and check."""
|
|
400
|
-
ctrl, app, client,
|
|
357
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
401
358
|
|
|
402
359
|
new_config = _make_config(update_channel='dev')
|
|
403
360
|
|
|
@@ -413,7 +370,7 @@ class TestSettingsChanged:
|
|
|
413
370
|
@staticmethod
|
|
414
371
|
def test_settings_changed_updates_auto_apply() -> None:
|
|
415
372
|
"""Changing settings should update the auto_apply flag."""
|
|
416
|
-
ctrl, app, client,
|
|
373
|
+
ctrl, app, client, banner, settings = _make_controller(auto_apply=True)
|
|
417
374
|
|
|
418
375
|
new_config = _make_config(auto_apply=False)
|
|
419
376
|
|
|
@@ -434,20 +391,26 @@ class TestSettingsChanged:
|
|
|
434
391
|
class TestCheckError:
|
|
435
392
|
"""Verify _on_check_error routes errors correctly."""
|
|
436
393
|
|
|
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
|
+
|
|
437
402
|
@staticmethod
|
|
438
403
|
def test_check_error_shows_banner_when_not_silent() -> None:
|
|
439
404
|
"""An exception during check should show banner when not silent."""
|
|
440
|
-
ctrl, app, client,
|
|
405
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
441
406
|
ctrl._on_check_error('timeout', silent=False)
|
|
442
407
|
|
|
443
|
-
assert
|
|
444
|
-
assert b2.state.name == 'ERROR'
|
|
408
|
+
assert banner.state.name == 'ERROR'
|
|
445
409
|
|
|
446
410
|
@staticmethod
|
|
447
411
|
def test_check_error_no_banner_when_silent() -> None:
|
|
448
412
|
"""An exception during check should NOT show banner when silent."""
|
|
449
|
-
ctrl, app, client,
|
|
413
|
+
ctrl, app, client, banner, settings = _make_controller()
|
|
450
414
|
ctrl._on_check_error('timeout', silent=True)
|
|
451
415
|
|
|
452
|
-
assert
|
|
453
|
-
assert b2.state.name == 'HIDDEN'
|
|
416
|
+
assert banner.state.name == 'HIDDEN'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/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.dev55 → synodic_client-0.0.1.dev56}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/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.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/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.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev55 → synodic_client-0.0.1.dev56}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|