synodic-client 0.0.1.dev68__tar.gz → 0.0.1.dev70__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.dev68 → synodic_client-0.0.1.dev70}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/pyproject.toml +3 -3
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/bootstrap.py +22 -6
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/init.py +2 -6
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/settings.py +4 -8
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/tray.py +38 -1
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/update_controller.py +10 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/startup.py +57 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/subprocess_patch.py +2 -2
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_settings.py +10 -14
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_update_controller.py +16 -20
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_init.py +17 -25
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/test_startup.py +125 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/README.md +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/action_card.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/theme.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/test_protocol.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev70
|
|
4
4
|
Author-Email: Synodic Software <contact@synodic.software>
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Project-URL: homepage, https://github.com/synodic/synodic-client
|
|
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
|
|
|
8
8
|
Requires-Python: <3.15,>=3.14
|
|
9
9
|
Requires-Dist: pyside6>=6.10.2
|
|
10
10
|
Requires-Dist: packaging>=26.0
|
|
11
|
-
Requires-Dist: porringer>=0.2.1.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev81
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
|
|
|
10
10
|
dependencies = [
|
|
11
11
|
"pyside6>=6.10.2",
|
|
12
12
|
"packaging>=26.0",
|
|
13
|
-
"porringer>=0.2.1.
|
|
13
|
+
"porringer>=0.2.1.dev81",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev70"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -36,7 +36,7 @@ build = [
|
|
|
36
36
|
]
|
|
37
37
|
lint = [
|
|
38
38
|
"ruff>=0.15.5",
|
|
39
|
-
"pyrefly>=0.
|
|
39
|
+
"pyrefly>=0.56.0",
|
|
40
40
|
]
|
|
41
41
|
test = [
|
|
42
42
|
"pytest>=9.0.2",
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -13,13 +13,25 @@ Import order matters:
|
|
|
13
13
|
5. import qt.application — PySide6 / porringer loaded here
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import logging
|
|
16
17
|
import sys
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
from synodic_client.
|
|
21
|
-
from synodic_client.
|
|
22
|
-
from synodic_client.
|
|
18
|
+
import traceback
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from synodic_client.config import set_dev_mode
|
|
22
|
+
from synodic_client.logging import configure_logging
|
|
23
|
+
from synodic_client.protocol import extract_uri_from_args
|
|
24
|
+
from synodic_client.subprocess_patch import apply as _apply_subprocess_patch
|
|
25
|
+
from synodic_client.updater import initialize_velopack
|
|
26
|
+
except Exception:
|
|
27
|
+
# Last-resort crash log when imports fail before logging is configured.
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
_fallback = os.path.join(os.environ.get('LOCALAPPDATA', '.'), 'Synodic', 'logs', 'bootstrap-crash.log')
|
|
31
|
+
os.makedirs(os.path.dirname(_fallback), exist_ok=True)
|
|
32
|
+
with open(_fallback, 'a', encoding='utf-8') as _f: # noqa: PTH123
|
|
33
|
+
_f.write(traceback.format_exc())
|
|
34
|
+
raise
|
|
23
35
|
|
|
24
36
|
# Parse flags early so logging uses the right filename and level.
|
|
25
37
|
_dev_mode = '--dev' in sys.argv[1:]
|
|
@@ -28,6 +40,10 @@ set_dev_mode(_dev_mode)
|
|
|
28
40
|
_apply_subprocess_patch()
|
|
29
41
|
|
|
30
42
|
configure_logging(debug=_debug)
|
|
43
|
+
|
|
44
|
+
_logger = logging.getLogger(__name__)
|
|
45
|
+
_logger.info('Bootstrap started (exe=%s, argv=%s)', sys.executable, sys.argv)
|
|
46
|
+
|
|
31
47
|
initialize_velopack()
|
|
32
48
|
|
|
33
49
|
if not _dev_mode:
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/init.py
RENAMED
|
@@ -19,7 +19,7 @@ import sys
|
|
|
19
19
|
|
|
20
20
|
from synodic_client.protocol import register_protocol
|
|
21
21
|
from synodic_client.resolution import resolve_config, seed_user_config_from_build
|
|
22
|
-
from synodic_client.startup import
|
|
22
|
+
from synodic_client.startup import sync_startup
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -59,10 +59,6 @@ def run_startup_preamble(exe_path: str | None = None) -> None:
|
|
|
59
59
|
register_protocol(exe_path)
|
|
60
60
|
|
|
61
61
|
config = resolve_config()
|
|
62
|
-
|
|
63
|
-
if config.auto_start:
|
|
64
|
-
register_startup(exe_path)
|
|
65
|
-
else:
|
|
66
|
-
remove_startup()
|
|
62
|
+
sync_startup(exe_path, auto_start=config.auto_start)
|
|
67
63
|
|
|
68
64
|
logger.info('Startup preamble complete (auto_start=%s, frozen=%s)', config.auto_start, frozen)
|
|
@@ -8,7 +8,7 @@ Updates* button with inline status feedback.
|
|
|
8
8
|
import logging
|
|
9
9
|
import sys
|
|
10
10
|
import traceback
|
|
11
|
-
from collections.abc import
|
|
11
|
+
from collections.abc import Generator
|
|
12
12
|
from contextlib import contextmanager
|
|
13
13
|
|
|
14
14
|
from PySide6.QtCore import Qt, QUrl, Signal
|
|
@@ -36,7 +36,7 @@ from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
|
36
36
|
from synodic_client.application.update_model import UpdateModel
|
|
37
37
|
from synodic_client.logging import log_path, set_debug_level
|
|
38
38
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
39
|
-
from synodic_client.startup import is_startup_registered,
|
|
39
|
+
from synodic_client.startup import is_startup_registered, sync_startup
|
|
40
40
|
|
|
41
41
|
logger = logging.getLogger(__name__)
|
|
42
42
|
|
|
@@ -334,7 +334,7 @@ class SettingsWindow(QMainWindow):
|
|
|
334
334
|
self._store.update(**changes)
|
|
335
335
|
|
|
336
336
|
@contextmanager
|
|
337
|
-
def _block_signals(self) ->
|
|
337
|
+
def _block_signals(self) -> Generator[None]:
|
|
338
338
|
"""Temporarily block signals on all settings controls."""
|
|
339
339
|
widgets = (
|
|
340
340
|
self._channel_combo,
|
|
@@ -382,11 +382,7 @@ class SettingsWindow(QMainWindow):
|
|
|
382
382
|
|
|
383
383
|
def _on_auto_start_changed(self, checked: bool) -> None:
|
|
384
384
|
self._store.update(auto_start=checked)
|
|
385
|
-
|
|
386
|
-
if checked:
|
|
387
|
-
register_startup(sys.executable)
|
|
388
|
-
else:
|
|
389
|
-
remove_startup()
|
|
385
|
+
sync_startup(sys.executable, auto_start=checked)
|
|
390
386
|
|
|
391
387
|
def _on_debug_logging_changed(self, checked: bool) -> None:
|
|
392
388
|
set_debug_level(enabled=checked)
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
from PySide6.QtCore import QTimer
|
|
6
7
|
from PySide6.QtGui import QAction
|
|
7
8
|
from PySide6.QtWidgets import (
|
|
8
9
|
QApplication,
|
|
@@ -54,7 +55,12 @@ class TrayScreen:
|
|
|
54
55
|
self.tray = QSystemTrayIcon()
|
|
55
56
|
self.tray.setIcon(self.tray_icon)
|
|
56
57
|
self.tray.activated.connect(self._on_tray_activated)
|
|
57
|
-
|
|
58
|
+
|
|
59
|
+
# At early Windows login the notification area may not be ready.
|
|
60
|
+
# Retry with back-off so the icon eventually appears.
|
|
61
|
+
self._tray_retry_count = 0
|
|
62
|
+
self._tray_retry_timer: QTimer | None = None
|
|
63
|
+
self._show_tray_icon()
|
|
58
64
|
|
|
59
65
|
self._build_menu(app, window)
|
|
60
66
|
|
|
@@ -127,6 +133,37 @@ class TrayScreen:
|
|
|
127
133
|
|
|
128
134
|
self.tray.setContextMenu(self.menu)
|
|
129
135
|
|
|
136
|
+
# Maximum number of tray-visibility retries at startup.
|
|
137
|
+
_TRAY_MAX_RETRIES = 5
|
|
138
|
+
# Delay between retries in milliseconds.
|
|
139
|
+
_TRAY_RETRY_DELAY_MS = 2000
|
|
140
|
+
|
|
141
|
+
def _show_tray_icon(self) -> None:
|
|
142
|
+
"""Show the tray icon, retrying if the system tray is not ready."""
|
|
143
|
+
if QSystemTrayIcon.isSystemTrayAvailable():
|
|
144
|
+
self.tray.setVisible(True)
|
|
145
|
+
logger.debug('System tray icon shown')
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if self._tray_retry_count < self._TRAY_MAX_RETRIES:
|
|
149
|
+
self._tray_retry_count += 1
|
|
150
|
+
logger.warning(
|
|
151
|
+
'System tray not available, retrying (%d/%d)',
|
|
152
|
+
self._tray_retry_count,
|
|
153
|
+
self._TRAY_MAX_RETRIES,
|
|
154
|
+
)
|
|
155
|
+
self._tray_retry_timer = QTimer()
|
|
156
|
+
self._tray_retry_timer.setSingleShot(True)
|
|
157
|
+
self._tray_retry_timer.timeout.connect(self._show_tray_icon)
|
|
158
|
+
self._tray_retry_timer.start(self._TRAY_RETRY_DELAY_MS)
|
|
159
|
+
else:
|
|
160
|
+
# Exhausted retries — show anyway as a best-effort fallback.
|
|
161
|
+
logger.warning(
|
|
162
|
+
'System tray still not available after %d retries, forcing visibility',
|
|
163
|
+
self._TRAY_MAX_RETRIES,
|
|
164
|
+
)
|
|
165
|
+
self.tray.setVisible(True)
|
|
166
|
+
|
|
130
167
|
def _on_tray_activated(self, reason: QSystemTrayIcon.ActivationReason) -> None:
|
|
131
168
|
"""Handle tray icon activation (e.g. double-click)."""
|
|
132
169
|
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
+
import sys
|
|
17
18
|
from collections.abc import Callable
|
|
18
19
|
from datetime import UTC, datetime
|
|
19
20
|
from typing import TYPE_CHECKING
|
|
@@ -34,6 +35,7 @@ from synodic_client.resolution import (
|
|
|
34
35
|
resolve_update_config,
|
|
35
36
|
)
|
|
36
37
|
from synodic_client.schema import UpdateInfo
|
|
38
|
+
from synodic_client.startup import sync_startup
|
|
37
39
|
|
|
38
40
|
if TYPE_CHECKING:
|
|
39
41
|
from synodic_client.application.config_store import ConfigStore
|
|
@@ -413,6 +415,14 @@ class UpdateController:
|
|
|
413
415
|
return
|
|
414
416
|
|
|
415
417
|
try:
|
|
418
|
+
# Re-register the startup entry with the current exe path so
|
|
419
|
+
# the registry value stays valid even if Velopack relocates
|
|
420
|
+
# the binary during the update. The relaunched process will
|
|
421
|
+
# overwrite it again via run_startup_preamble, but this
|
|
422
|
+
# ensures the entry is never stale between the update and
|
|
423
|
+
# the next launch.
|
|
424
|
+
sync_startup(sys.executable, auto_start=self._store.config.auto_start)
|
|
425
|
+
|
|
416
426
|
self._pending_version = None
|
|
417
427
|
self._client.apply_update_on_exit(restart=True, silent=silent)
|
|
418
428
|
logger.info('Update scheduled — restarting application')
|
|
@@ -96,6 +96,24 @@ if sys.platform == 'win32':
|
|
|
96
96
|
except OSError:
|
|
97
97
|
logger.exception('Failed to remove StartupApproved flag')
|
|
98
98
|
|
|
99
|
+
def get_registered_startup_path() -> str | None:
|
|
100
|
+
r"""Return the executable path stored in the ``Run`` registry key.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The unquoted path string, or ``None`` when the value does
|
|
104
|
+
not exist or cannot be read.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE) as key:
|
|
108
|
+
value, _ = winreg.QueryValueEx(key, STARTUP_VALUE_NAME)
|
|
109
|
+
# The value is stored as '"<path>"'; strip the quotes.
|
|
110
|
+
return value.strip('"') if isinstance(value, str) else None
|
|
111
|
+
except FileNotFoundError:
|
|
112
|
+
return None
|
|
113
|
+
except OSError:
|
|
114
|
+
logger.exception('Failed to read auto-startup path from registry')
|
|
115
|
+
return None
|
|
116
|
+
|
|
99
117
|
def is_startup_registered() -> bool:
|
|
100
118
|
r"""Check whether auto-startup is both present **and** enabled.
|
|
101
119
|
|
|
@@ -147,6 +165,14 @@ else:
|
|
|
147
165
|
"""Remove auto-startup registration (no-op on non-Windows)."""
|
|
148
166
|
logger.warning('Auto-startup removal is only supported on Windows (current: %s)', sys.platform)
|
|
149
167
|
|
|
168
|
+
def get_registered_startup_path() -> str | None:
|
|
169
|
+
"""Return the registered startup path (always ``None`` on non-Windows).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
``None``.
|
|
173
|
+
"""
|
|
174
|
+
return None
|
|
175
|
+
|
|
150
176
|
def is_startup_registered() -> bool:
|
|
151
177
|
"""Check auto-startup registration (always ``False`` on non-Windows).
|
|
152
178
|
|
|
@@ -154,3 +180,34 @@ else:
|
|
|
154
180
|
``False``.
|
|
155
181
|
"""
|
|
156
182
|
return False
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def sync_startup(exe_path: str, *, auto_start: bool) -> None:
|
|
186
|
+
"""Synchronise the auto-startup registry state with the given preference.
|
|
187
|
+
|
|
188
|
+
Registers or removes the startup entry and logs a warning when
|
|
189
|
+
the previously registered path differs from *exe_path* (stale
|
|
190
|
+
path after a Velopack update, for example).
|
|
191
|
+
|
|
192
|
+
This is a no-op when ``sys.frozen`` is falsy (non-installed
|
|
193
|
+
builds never touch the registry).
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
exe_path: Absolute path to the application executable.
|
|
197
|
+
auto_start: Whether auto-startup should be enabled.
|
|
198
|
+
"""
|
|
199
|
+
if not getattr(sys, 'frozen', False):
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
registered_path = get_registered_startup_path()
|
|
203
|
+
if registered_path and registered_path != exe_path:
|
|
204
|
+
logger.warning(
|
|
205
|
+
'Startup registry path mismatch: registered=%s, current=%s',
|
|
206
|
+
registered_path,
|
|
207
|
+
exe_path,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if auto_start:
|
|
211
|
+
register_startup(exe_path)
|
|
212
|
+
else:
|
|
213
|
+
remove_startup()
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/subprocess_patch.py
RENAMED
|
@@ -75,8 +75,8 @@ def _inject_hidden_flags(kwargs: dict[str, Any]) -> None:
|
|
|
75
75
|
def _patch_popen() -> None:
|
|
76
76
|
_original_init = subprocess.Popen.__init__
|
|
77
77
|
|
|
78
|
-
def _patched_init(self: subprocess.Popen, *args: Any, **kwargs: Any) -> None:
|
|
78
|
+
def _patched_init(self: subprocess.Popen, *args: Any, **kwargs: Any) -> None:
|
|
79
79
|
_inject_hidden_flags(kwargs)
|
|
80
80
|
_original_init(self, *args, **kwargs)
|
|
81
81
|
|
|
82
|
-
subprocess.Popen.__init__ = _patched_init
|
|
82
|
+
subprocess.Popen.__init__ = _patched_init
|
|
@@ -238,24 +238,23 @@ class TestSettingsCallbacks:
|
|
|
238
238
|
|
|
239
239
|
@staticmethod
|
|
240
240
|
def test_auto_start_registers_startup_when_frozen() -> None:
|
|
241
|
-
"""Enabling auto-start calls
|
|
241
|
+
"""Enabling auto-start calls sync_startup."""
|
|
242
242
|
config = _make_config()
|
|
243
243
|
window = _make_window(config)
|
|
244
244
|
|
|
245
245
|
new_config = _make_config(auto_start=True)
|
|
246
246
|
with (
|
|
247
247
|
patch.object(window._store, 'update', return_value=new_config),
|
|
248
|
-
patch('synodic_client.application.screen.settings.
|
|
248
|
+
patch('synodic_client.application.screen.settings.sync_startup') as mock_sync,
|
|
249
249
|
patch('synodic_client.application.screen.settings.is_startup_registered', return_value=False),
|
|
250
|
-
patch('synodic_client.application.screen.settings.getattr', return_value=True),
|
|
251
250
|
):
|
|
252
251
|
window._auto_start_check.setChecked(True)
|
|
253
252
|
|
|
254
|
-
|
|
253
|
+
mock_sync.assert_called_once()
|
|
255
254
|
|
|
256
255
|
@staticmethod
|
|
257
256
|
def test_auto_start_removes_startup_when_frozen() -> None:
|
|
258
|
-
"""Disabling auto-start calls
|
|
257
|
+
"""Disabling auto-start calls sync_startup with auto_start=False."""
|
|
259
258
|
config = _make_config(auto_start=True)
|
|
260
259
|
window = _make_window(config)
|
|
261
260
|
# Manually set initial state without triggering signals
|
|
@@ -266,31 +265,28 @@ class TestSettingsCallbacks:
|
|
|
266
265
|
new_config = _make_config(auto_start=False)
|
|
267
266
|
with (
|
|
268
267
|
patch.object(window._store, 'update', return_value=new_config),
|
|
269
|
-
patch('synodic_client.application.screen.settings.
|
|
270
|
-
patch('synodic_client.application.screen.settings.getattr', return_value=True),
|
|
268
|
+
patch('synodic_client.application.screen.settings.sync_startup') as mock_sync,
|
|
271
269
|
):
|
|
272
270
|
window._auto_start_check.setChecked(False)
|
|
273
271
|
|
|
274
|
-
|
|
272
|
+
mock_sync.assert_called_once()
|
|
275
273
|
|
|
276
274
|
@staticmethod
|
|
277
275
|
def test_auto_start_skips_registry_when_not_frozen() -> None:
|
|
278
|
-
"""Auto-start toggle persists config
|
|
276
|
+
"""Auto-start toggle persists config and delegates to sync_startup."""
|
|
279
277
|
config = _make_config()
|
|
280
278
|
window = _make_window(config)
|
|
281
279
|
|
|
282
280
|
new_config = _make_config(auto_start=True)
|
|
283
281
|
with (
|
|
284
282
|
patch.object(window._store, 'update', return_value=new_config),
|
|
285
|
-
patch('synodic_client.application.screen.settings.
|
|
286
|
-
patch('synodic_client.application.screen.settings.remove_startup') as mock_remove,
|
|
283
|
+
patch('synodic_client.application.screen.settings.sync_startup') as mock_sync,
|
|
287
284
|
patch('synodic_client.application.screen.settings.is_startup_registered', return_value=False),
|
|
288
|
-
patch('synodic_client.application.screen.settings.getattr', return_value=False),
|
|
289
285
|
):
|
|
290
286
|
window._auto_start_check.setChecked(True)
|
|
291
287
|
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
# sync_startup is always called — it handles the frozen guard internally
|
|
289
|
+
mock_sync.assert_called_once()
|
|
294
290
|
|
|
295
291
|
|
|
296
292
|
# ---------------------------------------------------------------------------
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_update_controller.py
RENAMED
|
@@ -28,26 +28,6 @@ from synodic_client.schema import (
|
|
|
28
28
|
# ---------------------------------------------------------------------------
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
class ModelSpy:
|
|
32
|
-
"""Records signal emissions from an :class:`UpdateModel`."""
|
|
33
|
-
|
|
34
|
-
def __init__(self, model: UpdateModel) -> None:
|
|
35
|
-
self.status: list[tuple[str, str]] = []
|
|
36
|
-
self.check_button_enabled: list[bool] = []
|
|
37
|
-
self.restart_visible: list[bool] = []
|
|
38
|
-
self.last_checked: list[str] = []
|
|
39
|
-
|
|
40
|
-
model.status_text_changed.connect(lambda t, s: self.status.append((t, s)))
|
|
41
|
-
model.check_button_enabled_changed.connect(self.check_button_enabled.append)
|
|
42
|
-
model.restart_visible_changed.connect(self.restart_visible.append)
|
|
43
|
-
model.last_checked_changed.connect(self.last_checked.append)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# ---------------------------------------------------------------------------
|
|
47
|
-
# Helpers
|
|
48
|
-
# ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
|
|
51
31
|
class ModelSpy:
|
|
52
32
|
"""Records signal emissions from an :class:`UpdateModel`."""
|
|
53
33
|
|
|
@@ -382,6 +362,22 @@ class TestApplyUpdate:
|
|
|
382
362
|
client.apply_update_on_exit.assert_not_called()
|
|
383
363
|
app.quit.assert_not_called()
|
|
384
364
|
|
|
365
|
+
@staticmethod
|
|
366
|
+
def test_apply_update_refreshes_startup_registry_when_frozen() -> None:
|
|
367
|
+
"""_apply_update should call sync_startup before quitting."""
|
|
368
|
+
ctrl, app, client, banner, model = _make_controller()
|
|
369
|
+
|
|
370
|
+
with (
|
|
371
|
+
patch('synodic_client.application.update_controller.sync_startup') as mock_sync,
|
|
372
|
+
patch('synodic_client.application.update_controller.sys') as mock_sys,
|
|
373
|
+
):
|
|
374
|
+
mock_sys.executable = r'C:\app\synodic.exe'
|
|
375
|
+
ctrl._apply_update()
|
|
376
|
+
|
|
377
|
+
mock_sync.assert_called_once_with(r'C:\app\synodic.exe', auto_start=True)
|
|
378
|
+
client.apply_update_on_exit.assert_called_once()
|
|
379
|
+
app.quit.assert_called_once()
|
|
380
|
+
|
|
385
381
|
|
|
386
382
|
# ---------------------------------------------------------------------------
|
|
387
383
|
# Settings changed → immediate check
|
|
@@ -26,8 +26,7 @@ class TestRunStartupPreamble:
|
|
|
26
26
|
patch(f'{_MODULE}.seed_user_config_from_build') as mock_seed,
|
|
27
27
|
patch(f'{_MODULE}.register_protocol') as mock_proto,
|
|
28
28
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
29
|
-
patch(f'{_MODULE}.
|
|
30
|
-
patch(f'{_MODULE}.remove_startup'),
|
|
29
|
+
patch(f'{_MODULE}.sync_startup'),
|
|
31
30
|
patch(f'{_MODULE}.getattr', return_value=True),
|
|
32
31
|
):
|
|
33
32
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
@@ -38,38 +37,34 @@ class TestRunStartupPreamble:
|
|
|
38
37
|
mock_resolve.assert_called_once()
|
|
39
38
|
|
|
40
39
|
@staticmethod
|
|
41
|
-
def
|
|
42
|
-
"""
|
|
40
|
+
def test_delegates_to_sync_startup_with_auto_start() -> None:
|
|
41
|
+
"""sync_startup is called with the resolved auto_start preference."""
|
|
43
42
|
with (
|
|
44
43
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
45
44
|
patch(f'{_MODULE}.register_protocol'),
|
|
46
45
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
47
|
-
patch(f'{_MODULE}.
|
|
48
|
-
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
46
|
+
patch(f'{_MODULE}.sync_startup') as mock_sync,
|
|
49
47
|
patch(f'{_MODULE}.getattr', return_value=True),
|
|
50
48
|
):
|
|
51
49
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
52
50
|
run_startup_preamble(r'C:\app\synodic.exe')
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
mock_remove.assert_not_called()
|
|
52
|
+
mock_sync.assert_called_once_with(r'C:\app\synodic.exe', auto_start=True)
|
|
56
53
|
|
|
57
54
|
@staticmethod
|
|
58
|
-
def
|
|
59
|
-
"""
|
|
55
|
+
def test_delegates_to_sync_startup_when_auto_start_false() -> None:
|
|
56
|
+
"""sync_startup receives auto_start=False when config says so."""
|
|
60
57
|
with (
|
|
61
58
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
62
59
|
patch(f'{_MODULE}.register_protocol'),
|
|
63
60
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
64
|
-
patch(f'{_MODULE}.
|
|
65
|
-
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
61
|
+
patch(f'{_MODULE}.sync_startup') as mock_sync,
|
|
66
62
|
patch(f'{_MODULE}.getattr', return_value=True),
|
|
67
63
|
):
|
|
68
64
|
mock_resolve.return_value = MagicMock(auto_start=False)
|
|
69
65
|
run_startup_preamble(r'C:\app\synodic.exe')
|
|
70
66
|
|
|
71
|
-
|
|
72
|
-
mock_register.assert_not_called()
|
|
67
|
+
mock_sync.assert_called_once_with(r'C:\app\synodic.exe', auto_start=False)
|
|
73
68
|
|
|
74
69
|
@staticmethod
|
|
75
70
|
def test_defaults_exe_path_to_sys_executable() -> None:
|
|
@@ -78,8 +73,7 @@ class TestRunStartupPreamble:
|
|
|
78
73
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
79
74
|
patch(f'{_MODULE}.register_protocol') as mock_proto,
|
|
80
75
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
81
|
-
patch(f'{_MODULE}.
|
|
82
|
-
patch(f'{_MODULE}.remove_startup'),
|
|
76
|
+
patch(f'{_MODULE}.sync_startup') as mock_sync,
|
|
83
77
|
patch(f'{_MODULE}.sys') as mock_sys,
|
|
84
78
|
patch(f'{_MODULE}.getattr', return_value=True),
|
|
85
79
|
):
|
|
@@ -88,25 +82,24 @@ class TestRunStartupPreamble:
|
|
|
88
82
|
run_startup_preamble()
|
|
89
83
|
|
|
90
84
|
mock_proto.assert_called_once_with(r'C:\Python\python.exe')
|
|
91
|
-
|
|
85
|
+
mock_sync.assert_called_once_with(r'C:\Python\python.exe', auto_start=True)
|
|
92
86
|
|
|
93
87
|
@staticmethod
|
|
94
|
-
def
|
|
95
|
-
"""Protocol
|
|
88
|
+
def test_skips_protocol_when_not_frozen() -> None:
|
|
89
|
+
"""Protocol registration is skipped in non-frozen builds."""
|
|
96
90
|
with (
|
|
97
91
|
patch(f'{_MODULE}.seed_user_config_from_build'),
|
|
98
92
|
patch(f'{_MODULE}.register_protocol') as mock_proto,
|
|
99
93
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
100
|
-
patch(f'{_MODULE}.
|
|
101
|
-
patch(f'{_MODULE}.remove_startup') as mock_remove,
|
|
94
|
+
patch(f'{_MODULE}.sync_startup') as mock_sync,
|
|
102
95
|
patch(f'{_MODULE}.getattr', return_value=False),
|
|
103
96
|
):
|
|
104
97
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
105
98
|
run_startup_preamble(r'C:\Python\python.exe')
|
|
106
99
|
|
|
107
100
|
mock_proto.assert_not_called()
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
# sync_startup is still called — it handles the frozen guard internally
|
|
102
|
+
mock_sync.assert_called_once()
|
|
110
103
|
|
|
111
104
|
@staticmethod
|
|
112
105
|
def test_idempotent_on_second_call() -> None:
|
|
@@ -115,8 +108,7 @@ class TestRunStartupPreamble:
|
|
|
115
108
|
patch(f'{_MODULE}.seed_user_config_from_build') as mock_seed,
|
|
116
109
|
patch(f'{_MODULE}.register_protocol'),
|
|
117
110
|
patch(f'{_MODULE}.resolve_config') as mock_resolve,
|
|
118
|
-
patch(f'{_MODULE}.
|
|
119
|
-
patch(f'{_MODULE}.remove_startup'),
|
|
111
|
+
patch(f'{_MODULE}.sync_startup'),
|
|
120
112
|
):
|
|
121
113
|
mock_resolve.return_value = MagicMock(auto_start=True)
|
|
122
114
|
run_startup_preamble(r'C:\app\synodic.exe')
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/test_startup.py
RENAMED
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
import winreg
|
|
4
4
|
from unittest.mock import MagicMock, patch
|
|
5
5
|
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
6
8
|
from synodic_client.startup import (
|
|
7
9
|
APPROVED_ENABLED,
|
|
8
10
|
RUN_KEY_PATH,
|
|
9
11
|
STARTUP_APPROVED_KEY_PATH,
|
|
10
12
|
STARTUP_VALUE_NAME,
|
|
13
|
+
get_registered_startup_path,
|
|
11
14
|
is_startup_registered,
|
|
12
15
|
register_startup,
|
|
13
16
|
remove_startup,
|
|
17
|
+
sync_startup,
|
|
14
18
|
)
|
|
15
19
|
|
|
16
20
|
|
|
@@ -238,3 +242,124 @@ class TestIsStartupRegistered:
|
|
|
238
242
|
patch.object(winreg, 'QueryValueEx', side_effect=FileNotFoundError),
|
|
239
243
|
):
|
|
240
244
|
assert is_startup_registered() is False
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class TestGetRegisteredStartupPath:
|
|
248
|
+
"""Tests for get_registered_startup_path."""
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def test_returns_unquoted_path() -> None:
|
|
252
|
+
"""Verify the returned path has surrounding quotes stripped."""
|
|
253
|
+
mock_key = MagicMock()
|
|
254
|
+
mock_key.__enter__ = MagicMock(return_value=mock_key)
|
|
255
|
+
mock_key.__exit__ = MagicMock(return_value=False)
|
|
256
|
+
|
|
257
|
+
with (
|
|
258
|
+
patch.object(winreg, 'OpenKey', return_value=mock_key),
|
|
259
|
+
patch.object(
|
|
260
|
+
winreg,
|
|
261
|
+
'QueryValueEx',
|
|
262
|
+
return_value=(r'"C:\Program Files\Synodic\synodic.exe"', winreg.REG_SZ),
|
|
263
|
+
),
|
|
264
|
+
):
|
|
265
|
+
assert get_registered_startup_path() == r'C:\Program Files\Synodic\synodic.exe'
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def test_returns_none_when_missing() -> None:
|
|
269
|
+
"""Verify None when the registry value does not exist."""
|
|
270
|
+
mock_key = MagicMock()
|
|
271
|
+
mock_key.__enter__ = MagicMock(return_value=mock_key)
|
|
272
|
+
mock_key.__exit__ = MagicMock(return_value=False)
|
|
273
|
+
|
|
274
|
+
with (
|
|
275
|
+
patch.object(winreg, 'OpenKey', return_value=mock_key),
|
|
276
|
+
patch.object(winreg, 'QueryValueEx', side_effect=FileNotFoundError),
|
|
277
|
+
):
|
|
278
|
+
assert get_registered_startup_path() is None
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def test_returns_none_on_os_error() -> None:
|
|
282
|
+
"""Verify None when an OSError prevents reading the registry."""
|
|
283
|
+
mock_key = MagicMock()
|
|
284
|
+
mock_key.__enter__ = MagicMock(return_value=mock_key)
|
|
285
|
+
mock_key.__exit__ = MagicMock(return_value=False)
|
|
286
|
+
|
|
287
|
+
with (
|
|
288
|
+
patch.object(winreg, 'OpenKey', return_value=mock_key),
|
|
289
|
+
patch.object(winreg, 'QueryValueEx', side_effect=OSError('access denied')),
|
|
290
|
+
):
|
|
291
|
+
assert get_registered_startup_path() is None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_SYNC_MODULE = 'synodic_client.startup'
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class TestSyncStartup:
|
|
298
|
+
"""Tests for sync_startup."""
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def test_registers_when_auto_start_true() -> None:
|
|
302
|
+
"""sync_startup calls register_startup when auto_start is True."""
|
|
303
|
+
with (
|
|
304
|
+
patch(f'{_SYNC_MODULE}.getattr', return_value=True),
|
|
305
|
+
patch(f'{_SYNC_MODULE}.get_registered_startup_path', return_value=None),
|
|
306
|
+
patch(f'{_SYNC_MODULE}.register_startup') as mock_reg,
|
|
307
|
+
patch(f'{_SYNC_MODULE}.remove_startup') as mock_rem,
|
|
308
|
+
):
|
|
309
|
+
sync_startup(r'C:\app\synodic.exe', auto_start=True)
|
|
310
|
+
|
|
311
|
+
mock_reg.assert_called_once_with(r'C:\app\synodic.exe')
|
|
312
|
+
mock_rem.assert_not_called()
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def test_removes_when_auto_start_false() -> None:
|
|
316
|
+
"""sync_startup calls remove_startup when auto_start is False."""
|
|
317
|
+
with (
|
|
318
|
+
patch(f'{_SYNC_MODULE}.getattr', return_value=True),
|
|
319
|
+
patch(f'{_SYNC_MODULE}.get_registered_startup_path', return_value=None),
|
|
320
|
+
patch(f'{_SYNC_MODULE}.register_startup') as mock_reg,
|
|
321
|
+
patch(f'{_SYNC_MODULE}.remove_startup') as mock_rem,
|
|
322
|
+
):
|
|
323
|
+
sync_startup(r'C:\app\synodic.exe', auto_start=False)
|
|
324
|
+
|
|
325
|
+
mock_rem.assert_called_once()
|
|
326
|
+
mock_reg.assert_not_called()
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def test_noop_when_not_frozen() -> None:
|
|
330
|
+
"""sync_startup is a no-op when sys.frozen is falsy."""
|
|
331
|
+
with (
|
|
332
|
+
patch(f'{_SYNC_MODULE}.getattr', return_value=False),
|
|
333
|
+
patch(f'{_SYNC_MODULE}.register_startup') as mock_reg,
|
|
334
|
+
patch(f'{_SYNC_MODULE}.remove_startup') as mock_rem,
|
|
335
|
+
):
|
|
336
|
+
sync_startup(r'C:\app\synodic.exe', auto_start=True)
|
|
337
|
+
|
|
338
|
+
mock_reg.assert_not_called()
|
|
339
|
+
mock_rem.assert_not_called()
|
|
340
|
+
|
|
341
|
+
@staticmethod
|
|
342
|
+
def test_logs_warning_on_path_mismatch(caplog: pytest.LogCaptureFixture) -> None:
|
|
343
|
+
"""A warning is logged when the registered path differs from exe_path."""
|
|
344
|
+
with (
|
|
345
|
+
patch(f'{_SYNC_MODULE}.getattr', return_value=True),
|
|
346
|
+
patch(f'{_SYNC_MODULE}.get_registered_startup_path', return_value=r'C:\old\synodic.exe'),
|
|
347
|
+
patch(f'{_SYNC_MODULE}.register_startup'),
|
|
348
|
+
patch(f'{_SYNC_MODULE}.remove_startup'),
|
|
349
|
+
):
|
|
350
|
+
sync_startup(r'C:\new\synodic.exe', auto_start=True)
|
|
351
|
+
|
|
352
|
+
assert 'mismatch' in caplog.text.lower()
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def test_no_warning_when_paths_match(caplog: pytest.LogCaptureFixture) -> None:
|
|
356
|
+
"""No warning when the registered path matches exe_path."""
|
|
357
|
+
with (
|
|
358
|
+
patch(f'{_SYNC_MODULE}.getattr', return_value=True),
|
|
359
|
+
patch(f'{_SYNC_MODULE}.get_registered_startup_path', return_value=r'C:\app\synodic.exe'),
|
|
360
|
+
patch(f'{_SYNC_MODULE}.register_startup'),
|
|
361
|
+
patch(f'{_SYNC_MODULE}.remove_startup'),
|
|
362
|
+
):
|
|
363
|
+
sync_startup(r'C:\app\synodic.exe', auto_start=True)
|
|
364
|
+
|
|
365
|
+
assert 'mismatch' not in caplog.text.lower()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/config_store.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/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
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/theme.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/synodic_client/application/update_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/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
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/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
|
{synodic_client-0.0.1.dev68 → synodic_client-0.0.1.dev70}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|