feat: 完成PySide6 worker基座
- 新增 BaseWorker 通用 signals、取消标记和统一 run 入口 - 新增 run_worker QThread 包装与生命周期清理 - 新增 worker 单元测试覆盖 signal、取消、异常和类型校验 - 更新任务看板、API 合约、架构、当前状态和进度记录
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user