ForcePilot/backend/test/unit/scheduler/conftest.py

233 lines
6.9 KiB
Python
Raw Normal View History

"""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