ForcePilot/backend/test/unit/scheduler/conftest.py
Kris 3ae9c0bd21 test: 批量修复与新增单元测试用例,清理废弃测试目录
1.  删除了 test/unit/external_systems/framework/ 下的废弃空测试目录
2.  修复多处测试断言逻辑、参数传递与测试数据构造
3.  新增日志级别、缓存令牌、流事件等DTO单元测试
4.  补充路由绑定、会话仓储、outbox仓储的测试覆盖
5.  更新测试用例中的异常类型、参数校验与业务逻辑断言
2026-07-11 21:43:16 +08:00

237 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""scheduler 限界上下文单元测试共享 fixture。
本文件仅放置跨多个测试文件复用的 fixture 与工厂函数。单文件独有的 helper
应保留在对应测试文件内,避免过度集中(见 testing-guidelines.md
"""
from __future__ import annotations
from datetime import datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.scheduler.core.models import (
DailyStat,
HandlerSummary,
ScheduledTask,
ScheduledTaskRunLog,
)
# ─── 工厂函数 ─────────────────────────────────────────────────────────────
@pytest.fixture
def make_task() -> type[ScheduledTask]:
"""返回 ScheduledTask 工厂类型,调用时按需覆盖字段。
用法::
task = make_task(task_id="abc", status="paused")
"""
def _build(**overrides: Any) -> ScheduledTask:
defaults: dict[str, Any] = {
"task_id": "task-001",
"handler_name": "test_handler",
"owner_scope": "system",
"owner_id": "test:owner-1",
"schedule_kind": "cron",
"cron_expression": "*/15 * * * *",
"tz": "Asia/Shanghai",
"payload": {"key": "value"},
"enabled": True,
"delete_after_run": False,
"block_strategy": "discard_later",
"stagger_seconds": 30,
"consecutive_errors": 0,
"status": "active",
"next_run_at": datetime(2024, 1, 1, 0, 15, 0),
"id": 1,
}
defaults.update(overrides)
return ScheduledTask(**defaults)
return _build
@pytest.fixture
def make_run_log() -> type[ScheduledTaskRunLog]:
"""返回 ScheduledTaskRunLog 工厂类型。"""
def _build(**overrides: Any) -> ScheduledTaskRunLog:
defaults: dict[str, Any] = {
"task_id": "task-001",
"run_id": "run-001",
"triggered_by": "auto",
"status": "running",
"started_at": datetime(2024, 1, 1, 0, 15, 0),
"id": 1,
}
defaults.update(overrides)
return ScheduledTaskRunLog(**defaults)
return _build
@pytest.fixture
def make_handler_summary() -> type[HandlerSummary]:
"""返回 HandlerSummary 工厂类型。"""
def _build(**overrides: Any) -> HandlerSummary:
defaults: dict[str, Any] = {
"name": "test_handler",
"task_count": 5,
"active_task_count": 3,
"dead_letter_count": 0,
"last_active_at": datetime(2024, 1, 1, 0, 15, 0),
}
defaults.update(overrides)
return HandlerSummary(**defaults)
return _build
@pytest.fixture
def make_daily_stat() -> type[DailyStat]:
"""返回 DailyStat 工厂类型。"""
def _build(**overrides: Any) -> DailyStat:
defaults: dict[str, Any] = {
"stat_date": datetime(2024, 1, 1).date(),
"handler_name": "test_handler",
"success_count": 10,
"failure_count": 2,
"timeout_count": 1,
"dead_letter_count": 0,
}
defaults.update(overrides)
return DailyStat(**defaults)
return _build
# ─── Stub 端口实现 ─────────────────────────────────────────────────────────
class FakeRepositories:
"""内存版 Repositories 桩,所有方法均为 AsyncMock。
通过 ``task`` / ``run_log`` / ``run_log_daily`` / ``idempotency`` 属性
访问各仓储桩,可按需配置 ``return_value`` / ``side_effect``。
"""
def __init__(self) -> None:
self.task = AsyncMock()
self.run_log = AsyncMock()
self.run_log_daily = AsyncMock()
self.idempotency = AsyncMock()
class FakeRuntimeServices:
"""内存版 RuntimeServices 桩。
- ``handler_registry````MagicMock`` 或 None模拟 api 进程)
- ``arq_pool````AsyncMock`` 或 None模拟 api 进程)
- ``config````MagicMock``,按需配置 ``scheduler_*`` 字段
"""
def __init__(self) -> None:
self.handler_registry = MagicMock()
self.arq_pool = AsyncMock()
# 默认配置与 app.py 默认值对齐
self.config = MagicMock()
self.config.scheduler_enabled = True
self.config.scheduler_tick_interval_seconds = 60
self.config.scheduler_max_payload_size_kb = 64
self.config.scheduler_min_cron_interval_minutes = 10
self.config.scheduler_stagger_max_seconds = 300
self.config.scheduler_backoff_schedule = [10, 30, 120, 600, 3600]
self.config.scheduler_max_consecutive_errors = 5
self.config.scheduler_task_timeout_seconds = 600
self.config.scheduler_run_log_retention_days = 90
self.config.scheduler_idempotency_retention_hours = 24
class FakeUnitOfWork:
"""内存版 UnitOfWork 桩,记录 commit / rollback 调用。
``async with`` 进入时返回 self正常退出调用 ``commit``,异常退出调用
``rollback``,与 ``SqlAlchemyUnitOfWork`` 行为一致。
"""
def __init__(self) -> None:
self.committed = False
self.rolled_back = False
self.enter_count = 0
self.exit_count = 0
async def __aenter__(self) -> FakeUnitOfWork:
self.enter_count += 1
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
self.exit_count += 1
if exc_type is not None:
self.rolled_back = True
else:
self.committed = True
async def commit(self) -> None:
self.committed = True
async def rollback(self) -> None:
self.rolled_back = True
@pytest.fixture
def fake_repos() -> FakeRepositories:
"""提供内存版 Repositories 桩。"""
return FakeRepositories()
@pytest.fixture
def fake_runtime() -> FakeRuntimeServices:
"""提供内存版 RuntimeServices 桩。"""
return FakeRuntimeServices()
@pytest.fixture
def fake_uow() -> FakeUnitOfWork:
"""提供内存版 UnitOfWork 桩。"""
return FakeUnitOfWork()
@pytest.fixture
def fixed_now(monkeypatch: pytest.MonkeyPatch) -> datetime:
"""固定 UTC naive 时间,用于时间相关测试的稳定基准。
同时 monkeypatch ``scheduler_service`` 模块内的 ``utc_now_naive``
使被测服务消费到同一固定时间。
"""
fixed = datetime(2024, 6, 1, 0, 0, 0)
import yuxi.scheduler.use_cases.services.scheduler_service as svc_mod
monkeypatch.setattr(svc_mod, "utc_now_naive", lambda: fixed)
return fixed
@pytest.fixture
def fixed_uuid(monkeypatch: pytest.MonkeyPatch) -> str:
"""固定 ``uuid.uuid4().hex`` 返回值,保证 task_id / run_id 可重复。
被 ``scheduler_service`` 模块内的 ``uuid.uuid4`` 调用消费。
"""
fixed_hex = "fixed-uuid-hex-001"
class _FakeUUID:
hex = fixed_hex
monkeypatch.setattr(
"yuxi.scheduler.use_cases.services.scheduler_service.uuid.uuid4",
lambda: _FakeUUID(),
)
return fixed_hex