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