import asyncio import threading from uuid import uuid4 from app.core.config import global_vars from app.scheduler import Scheduler def _build_scheduler(job_id, func): """构造不启动 APScheduler 的定时服务测试对象。""" scheduler = object.__new__(Scheduler) scheduler._lock = threading.RLock() scheduler._jobs = { job_id: { "name": "测试定时服务", "provider_name": "测试", "func": func, "running": False, } } return scheduler def test_scheduler_records_live_and_completed_progress(): """定时服务应在执行中更新进度,并在成功后收敛为 100%。""" job_id = f"test-success-{uuid4()}" snapshots = [] def task(progress_callback): """上报一次中间进度。""" progress_callback(value=42, text="正在处理", data={"finished": 2}) snapshots.append(scheduler.get_progress(job_id)) scheduler = _build_scheduler(job_id, task) scheduler.start(job_id) assert snapshots[0].enable is True assert snapshots[0].value == 42 assert snapshots[0].status == "running" assert snapshots[0].data["finished"] == 2 progress = scheduler.get_progress(job_id) assert progress.enable is False assert progress.value == 100 assert progress.status == "success" assert progress.success is True assert progress.started_at assert progress.finished_at def test_scheduler_failure_preserves_last_progress(monkeypatch): """定时服务异常时应保留失败位置,而不是伪装成 100% 完成。""" job_id = f"test-failure-{uuid4()}" def task(progress_callback): """上报进度后抛出异常。""" progress_callback(value=37, text="处理失败") raise RuntimeError("预期失败") scheduler = _build_scheduler(job_id, task) monkeypatch.setattr( scheduler, "_Scheduler__handle_job_error", lambda **kwargs: None, ) scheduler.start(job_id) progress = scheduler.get_progress(job_id) assert progress.enable is False assert progress.value == 37 assert progress.status == "failed" assert progress.success is False assert progress.error == "预期失败" def test_scheduler_treats_standard_failure_result_as_failed(): """返回 `(False, message)` 的定时服务应记录为业务失败。""" job_id = f"test-result-failure-{uuid4()}" def task(progress_callback): """返回标准失败结果。""" progress_callback(value=55, text="业务校验失败") return False, "业务失败" scheduler = _build_scheduler(job_id, task) scheduler.start(job_id) progress = scheduler.get_progress(job_id) assert progress.value == 55 assert progress.status == "failed" assert progress.error == "业务失败" def test_scheduler_runs_async_job_without_running_global_loop(monkeypatch): """全局事件循环未运行时,异步定时服务仍应正常执行并收敛进度。""" job_id = f"test-async-{uuid4()}" async def task(progress_callback): """上报异步任务进度。""" progress_callback(value=65, text="异步处理中") scheduler = _build_scheduler(job_id, task) target_loop = asyncio.new_event_loop() monkeypatch.setattr(global_vars, "CURRENT_EVENT_LOOP", target_loop) try: scheduler.start(job_id) finally: target_loop.close() progress = scheduler.get_progress(job_id) assert progress.enable is False assert progress.value == 100 assert progress.status == "success" def test_scheduler_runs_async_job_from_current_event_loop(monkeypatch): """在异步入口中手动触发定时服务时,不应嵌套调用 `asyncio.run`。""" job_id = f"test-current-loop-{uuid4()}" async def task(progress_callback): """在当前事件循环中上报进度。""" progress_callback(value=75, text="当前循环处理中") async def run_task(): """从已运行的事件循环启动定时服务。""" scheduler.start(job_id) await asyncio.sleep(0) scheduler = _build_scheduler(job_id, task) target_loop = asyncio.new_event_loop() monkeypatch.setattr(global_vars, "CURRENT_EVENT_LOOP", target_loop) try: asyncio.run(run_task()) finally: target_loop.close() progress = scheduler.get_progress(job_id) assert progress.enable is False assert progress.value == 100 assert progress.status == "success" def test_scheduler_returns_none_for_unknown_job(): """未注册且无历史进度的定时服务应返回空。""" job_id = f"test-unknown-{uuid4()}" scheduler = object.__new__(Scheduler) scheduler._lock = threading.RLock() scheduler._jobs = {} assert scheduler.get_progress(job_id) is None