ForcePilot/backend/test/unit/scheduler/conftest.py
Kris 9e68234c20 test: 新增 scheduler 限界上下文全量单元测试
新增覆盖以下模块的单元测试:
1. 核心契约与校验函数
2. 持久化层工作单元与ORM映射
3. 运行时处理器注册表
4. 业务用例工具类(退避、抖动、Cron计算)
5. 基础设施容器装配
6. 框架内置清理处理器(运行日志、幂等记录)
7. 测试共享Fixture与桩实现
2026-06-22 21:16:16 +08:00

233 lines
6.9 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,
"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_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