feat(update): add forced upgrade progress flow
This commit is contained in:
+148
-60
@@ -3,6 +3,7 @@ import unittest
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
@@ -12,7 +13,17 @@ sys.path.insert(0, os.path.dirname(__file__))
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import gui
|
||||
from app import accounts, ai, appconfig, db, image_paths, image_studio, prompts, update_check
|
||||
from app import (
|
||||
accounts,
|
||||
ai,
|
||||
appconfig,
|
||||
db,
|
||||
image_paths,
|
||||
image_studio,
|
||||
prompts,
|
||||
update_check,
|
||||
update_installer,
|
||||
)
|
||||
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
@@ -42,12 +53,14 @@ from app.gui import (
|
||||
CollectTab,
|
||||
GenerateWorker,
|
||||
GenerateTab,
|
||||
ForcedUpdateDialog,
|
||||
ImageStudioTab,
|
||||
MainWindow,
|
||||
SettingsTab,
|
||||
TAB_STYLE,
|
||||
TAB_TITLES,
|
||||
WriteBackWorker,
|
||||
UpdatePreparationWorker,
|
||||
)
|
||||
from app.gui import file_manager
|
||||
import app.gui.workers as gui_workers
|
||||
@@ -1573,82 +1586,157 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertNotIn("/api/v1", message)
|
||||
self.assertNotIn("/generated/images", message)
|
||||
|
||||
def test_startup_update_gate_forced_blocks_and_opens_download(self):
|
||||
boxes = []
|
||||
def test_startup_update_gate_forced_blocks_and_uses_upgrade_dialog(self):
|
||||
dialogs = []
|
||||
|
||||
class FakeButton:
|
||||
def __init__(self, label):
|
||||
self.label = label
|
||||
self.enabled = True
|
||||
|
||||
def setEnabled(self, enabled):
|
||||
self.enabled = enabled
|
||||
|
||||
class FakeMessageBox:
|
||||
Warning = object()
|
||||
AcceptRole = object()
|
||||
RejectRole = object()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
class FakeDialog:
|
||||
def __init__(self, result, parent=None):
|
||||
self.result = result
|
||||
self.parent = parent
|
||||
self.icon = None
|
||||
self.title = ""
|
||||
self.text = ""
|
||||
self.informative_text = ""
|
||||
self.buttons = {}
|
||||
self.default_button = None
|
||||
boxes.append(self)
|
||||
|
||||
def setIcon(self, icon):
|
||||
self.icon = icon
|
||||
|
||||
def setWindowTitle(self, title):
|
||||
self.title = title
|
||||
|
||||
def setText(self, text):
|
||||
self.text = text
|
||||
|
||||
def setInformativeText(self, text):
|
||||
self.informative_text = text
|
||||
|
||||
def addButton(self, label, role):
|
||||
button = FakeButton(label)
|
||||
self.buttons[label] = button
|
||||
return button
|
||||
|
||||
def setDefaultButton(self, button):
|
||||
self.default_button = button
|
||||
self.executed = False
|
||||
dialogs.append(self)
|
||||
|
||||
def exec(self):
|
||||
self.executed = True
|
||||
return 0
|
||||
|
||||
def clickedButton(self):
|
||||
return self.buttons["下载新版"]
|
||||
|
||||
result = update_check.UpdateCheckResult(
|
||||
current_version="1.0.0",
|
||||
checked=True,
|
||||
forced=True,
|
||||
latest_version="1.2.0",
|
||||
min_supported_version="1.1.0",
|
||||
download_url="https://example.test/cmshopee.zip",
|
||||
download_url="https://cm.833729.com/cmshopee.zip",
|
||||
sha256="a" * 64,
|
||||
size_bytes=123,
|
||||
package_format="cmshopee-portable-v1",
|
||||
updater_protocol=1,
|
||||
min_updater_protocol=1,
|
||||
message="必须升级",
|
||||
)
|
||||
opened = []
|
||||
|
||||
with mock.patch("app.gui.QMessageBox", FakeMessageBox):
|
||||
allowed = gui._run_startup_update_gate(
|
||||
checker=lambda: result,
|
||||
opener=opened.append,
|
||||
)
|
||||
allowed = gui._run_startup_update_gate(
|
||||
checker=lambda: result,
|
||||
dialog_factory=FakeDialog,
|
||||
)
|
||||
|
||||
self.assertFalse(allowed)
|
||||
self.assertEqual(["https://example.test/cmshopee.zip"], opened)
|
||||
self.assertEqual("必须升级", boxes[0].title)
|
||||
self.assertIn("当前版本:1.0.0", boxes[0].informative_text)
|
||||
self.assertIn("线上版本:1.2.0", boxes[0].informative_text)
|
||||
self.assertIn("保留 data/ 目录", boxes[0].informative_text)
|
||||
self.assertEqual(boxes[0].buttons["下载新版"], boxes[0].default_button)
|
||||
self.assertEqual(1, len(dialogs))
|
||||
self.assertTrue(dialogs[0].executed)
|
||||
self.assertIs(result, dialogs[0].result)
|
||||
|
||||
def test_forced_update_dialog_prepares_and_launches_updater_without_blocking_ui(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
install_root = os.path.join(temp_dir, "install")
|
||||
staging_root = os.path.join(install_root, ".cmshopee-update", "staging", "1.2.0-test")
|
||||
os.makedirs(staging_root)
|
||||
result = update_check.UpdateCheckResult(
|
||||
current_version="1.0.0",
|
||||
checked=True,
|
||||
forced=True,
|
||||
latest_version="1.2.0",
|
||||
download_url="https://cm.833729.com/cmshopee.zip",
|
||||
sha256="a" * 64,
|
||||
size_bytes=100,
|
||||
package_format="cmshopee-portable-v1",
|
||||
updater_protocol=1,
|
||||
min_updater_protocol=1,
|
||||
message="修复并优化自动升级",
|
||||
)
|
||||
staged = SimpleNamespace(
|
||||
version="1.2.0",
|
||||
staging_dir=os.path.abspath(staging_root),
|
||||
)
|
||||
|
||||
def prepare(_metadata, _install_root, **callbacks):
|
||||
callbacks["stage_callback"]("正在下载新版")
|
||||
callbacks["progress"](50, 100)
|
||||
return staged
|
||||
|
||||
launched = []
|
||||
def factory(info, root):
|
||||
return UpdatePreparationWorker(info, root, prepare=prepare)
|
||||
|
||||
dialog = ForcedUpdateDialog(
|
||||
result,
|
||||
install_root=install_root,
|
||||
worker_factory=factory,
|
||||
updater_launcher=lambda updater, plan: launched.append((updater, plan)),
|
||||
)
|
||||
self.addCleanup(dialog.close)
|
||||
with mock.patch(
|
||||
"app.gui.update_dialog.updater_entry.create_plan",
|
||||
return_value=os.path.join(temp_dir, "plan.json"),
|
||||
):
|
||||
dialog.start_update()
|
||||
deadline = time.time() + 3
|
||||
while dialog.thread is not None and time.time() < deadline:
|
||||
self.app.processEvents()
|
||||
time.sleep(0.01)
|
||||
|
||||
self.assertIsNone(dialog.thread)
|
||||
self.assertTrue(dialog.update_started)
|
||||
self.assertEqual(100, dialog.progress_bar.value())
|
||||
self.assertIn("50 B / 100 B", dialog.bytes_label.text())
|
||||
self.assertEqual(1, len(launched))
|
||||
self.assertIn("cmshopee-updater.exe", str(launched[0][0]))
|
||||
|
||||
def test_forced_update_dialog_cancel_waits_for_worker_cleanup(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
result = update_check.UpdateCheckResult(
|
||||
current_version="1.0.0",
|
||||
checked=True,
|
||||
forced=True,
|
||||
latest_version="1.2.0",
|
||||
download_url="https://cm.833729.com/cmshopee.zip",
|
||||
sha256="a" * 64,
|
||||
size_bytes=100,
|
||||
package_format="cmshopee-portable-v1",
|
||||
updater_protocol=1,
|
||||
min_updater_protocol=1,
|
||||
)
|
||||
|
||||
def prepare(_metadata, _install_root, **callbacks):
|
||||
while not callbacks["cancelled"]():
|
||||
time.sleep(0.01)
|
||||
raise update_installer.UpdateCancelled("已取消下载新版")
|
||||
|
||||
def factory(info, root):
|
||||
return UpdatePreparationWorker(info, root, prepare=prepare)
|
||||
|
||||
dialog = ForcedUpdateDialog(
|
||||
result,
|
||||
install_root=temp_dir,
|
||||
worker_factory=factory,
|
||||
)
|
||||
self.addCleanup(dialog.close)
|
||||
dialog.start_update()
|
||||
deadline = time.time() + 1
|
||||
while dialog.worker is None and time.time() < deadline:
|
||||
self.app.processEvents()
|
||||
dialog.request_exit()
|
||||
deadline = time.time() + 3
|
||||
while dialog.thread is not None and time.time() < deadline:
|
||||
self.app.processEvents()
|
||||
time.sleep(0.01)
|
||||
|
||||
self.assertIsNone(dialog.thread)
|
||||
self.assertIsNone(dialog.worker)
|
||||
|
||||
def test_forced_update_dialog_blocks_when_auto_install_metadata_is_incomplete(self):
|
||||
result = update_check.UpdateCheckResult(
|
||||
current_version="1.0.0",
|
||||
checked=True,
|
||||
forced=True,
|
||||
latest_version="1.2.0",
|
||||
download_url="https://cm.833729.com/cmshopee.zip",
|
||||
)
|
||||
dialog = ForcedUpdateDialog(result)
|
||||
self.addCleanup(dialog.close)
|
||||
|
||||
self.assertIn("校验值缺失", dialog.stage_label.text())
|
||||
self.assertEqual("重试", dialog.action_button.text())
|
||||
self.assertIsNone(dialog.thread)
|
||||
|
||||
def test_startup_update_gate_check_failure_allows_entry_and_logs(self):
|
||||
result = update_check.UpdateCheckResult(
|
||||
|
||||
Reference in New Issue
Block a user