feat: 完成PySide6 worker基座

- 新增 BaseWorker 通用 signals、取消标记和统一 run 入口

- 新增 run_worker QThread 包装与生命周期清理

- 新增 worker 单元测试覆盖 signal、取消、异常和类型校验

- 更新任务看板、API 合约、架构、当前状态和进度记录
This commit is contained in:
chengma
2026-06-27 10:05:29 +08:00
parent b8318c881d
commit 479ab5c41d
7 changed files with 276 additions and 12 deletions
+117
View File
@@ -0,0 +1,117 @@
import os
import unittest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from _helpers import REPO_ROOT # noqa: F401
from app import workers
if workers.QT_IMPORT_ERROR is not None:
raise unittest.SkipTest("PySide6 未安装")
from PySide6.QtCore import QEventLoop, QTimer
from PySide6.QtWidgets import QApplication
from app.workers import BaseWorker, run_worker
class DemoWorker(BaseWorker):
def execute(self):
self.log.emit("开始")
self.progress.emit({"done": 1, "total": 1})
self.row_updated.emit(7, {"status": "success"})
return {"ok": True, "done": 1}
class CancelAwareWorker(BaseWorker):
def execute(self):
if self.should_cancel():
return {"done": 0}
self.cancel()
return {"done": 0}
class FailingWorker(BaseWorker):
def execute(self):
raise RuntimeError("模拟失败")
class WorkerTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.app = QApplication.instance() or QApplication([])
def start_and_wait(self, thread, timeout_ms=2000):
loop = QEventLoop()
finished = []
def on_finished():
finished.append(True)
loop.quit()
timer = QTimer()
timer.setSingleShot(True)
timer.timeout.connect(loop.quit)
thread.finished.connect(on_finished)
timer.start(timeout_ms)
thread.start()
loop.exec()
self.assertTrue(finished, "worker thread did not finish before timeout")
def test_worker_emits_common_signals_and_finishes(self):
worker = DemoWorker()
logs = []
progress = []
rows = []
finished = []
worker.log.connect(logs.append)
worker.progress.connect(lambda payload: progress.append(dict(payload)))
worker.row_updated.connect(lambda task_id, fields: rows.append((task_id, dict(fields))))
worker.finished.connect(lambda payload: finished.append(dict(payload)))
thread = run_worker(worker, start=False)
self.start_and_wait(thread)
self.assertEqual(["开始"], logs)
self.assertEqual([{"done": 1, "total": 1}], progress)
self.assertEqual([(7, {"status": "success"})], rows)
self.assertEqual([{"ok": True, "done": 1}], finished)
def test_cancel_flag_emits_cancelled_instead_of_finished(self):
worker = CancelAwareWorker()
cancelled = []
finished = []
worker.cancelled.connect(lambda payload: cancelled.append(dict(payload)))
worker.finished.connect(lambda payload: finished.append(dict(payload)))
thread = run_worker(worker, start=False)
self.start_and_wait(thread)
self.assertEqual([{"done": 0, "cancelled": True}], cancelled)
self.assertEqual([], finished)
def test_uncaught_exception_emits_failed_and_finished_summary(self):
worker = FailingWorker()
failed = []
finished = []
worker.failed.connect(lambda task_id, error: failed.append((task_id, error)))
worker.finished.connect(lambda payload: finished.append(dict(payload)))
thread = run_worker(worker, start=False)
self.start_and_wait(thread)
self.assertEqual([(-1, "模拟失败")], failed)
self.assertEqual([{"ok": False, "error": "模拟失败"}], finished)
def test_run_worker_rejects_plain_object(self):
with self.assertRaises(TypeError):
run_worker(object(), start=False)
if __name__ == "__main__":
unittest.main()