ForcePilot/backend/test/unit/scheduler/adapters/persistence/test_mappers.py

287 lines
9.8 KiB
Python
Raw Normal View History

"""ORM ↔ dataclass 映射边界单元测试。
覆盖 ``yuxi.scheduler.adapters.persistence.mappers`` 模块
- ``orm_to_task``ORM ScheduledTask core dataclass
- ``orm_to_run_log``ORM ScheduledTaskRunLog core dataclass
- ``daily_stat_row_to_dataclass``仓储层 DailyStat core dataclass
- ``handler_summary_row_to_dataclass``仓储层 HandlerSummary core dataclass
- None 入参返回 None
"""
from __future__ import annotations
from datetime import datetime
from types import SimpleNamespace
import pytest
from yuxi.scheduler.adapters.persistence.mappers import (
daily_stat_row_to_dataclass,
handler_summary_row_to_dataclass,
orm_to_run_log,
orm_to_task,
)
from yuxi.scheduler.core.models import (
DailyStat,
HandlerSummary,
ScheduledTask,
ScheduledTaskRunLog,
)
pytestmark = pytest.mark.unit
# ─── 测试辅助 ─────────────────────────────────────────────────────────────
def _make_task_orm(**overrides) -> SimpleNamespace:
"""构造类似 ORM ScheduledTask 的 SimpleNamespace。"""
defaults = {
"id": 1,
"task_id": "task-001",
"handler_name": "my_handler",
"owner_scope": "system",
"owner_id": "test:1",
"schedule_kind": "cron",
"cron_expression": "*/15 * * * *",
"run_at": None,
"tz": "Asia/Shanghai",
"payload": {"k": "v"},
"enabled": True,
"delete_after_run": False,
"block_strategy": "discard_later",
"stagger_seconds": 30,
"consecutive_errors": 0,
"status": "active",
"last_run_at": None,
"next_run_at": datetime(2024, 1, 1, 0, 15, 0),
"last_error": None,
"created_by": "system",
"updated_by": None,
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"updated_at": None,
"is_deleted": 0,
"deleted_at": None,
}
defaults.update(overrides)
return SimpleNamespace(**defaults)
def _make_run_log_orm(**overrides) -> SimpleNamespace:
"""构造类似 ORM ScheduledTaskRunLog 的 SimpleNamespace。"""
defaults = {
"id": 1,
"task_id": "task-001",
"run_id": "run-001",
"triggered_by": "auto",
"status": "success",
"error_message": None,
"output": {"rows": 10},
"started_at": datetime(2024, 1, 1, 0, 15, 0),
"finished_at": datetime(2024, 1, 1, 0, 15, 5),
"created_by": "system",
"updated_by": None,
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"updated_at": None,
"is_deleted": 0,
"deleted_at": None,
}
defaults.update(overrides)
return SimpleNamespace(**defaults)
# ─── orm_to_task ──────────────────────────────────────────────────────────
@pytest.mark.unit
class TestOrmToTask:
def test_none_returns_none(self):
# Arrange & Act & Assert - 入参为 None 时返回 None
assert orm_to_task(None) is None
def test_basic_mapping(self):
# Arrange
orm = _make_task_orm()
# Act
task = orm_to_task(orm)
# Assert
assert isinstance(task, ScheduledTask)
assert task.id == 1
assert task.task_id == "task-001"
assert task.handler_name == "my_handler"
assert task.owner_scope == "system"
assert task.schedule_kind == "cron"
assert task.cron_expression == "*/15 * * * *"
assert task.payload == {"k": "v"}
assert task.status == "active"
assert task.next_run_at == datetime(2024, 1, 1, 0, 15, 0)
def test_none_payload_becomes_empty_dict(self):
# Arrange - ORM payload 为 None 时 mapper 兜底为空 dict
orm = _make_task_orm(payload=None)
# Act
task = orm_to_task(orm)
# Assert
assert task.payload == {}
def test_all_fields_mapped(self):
# Arrange - 验证所有字段都被映射
orm = _make_task_orm()
# Act
task = orm_to_task(orm)
# Assert
assert task.id == orm.id
assert task.task_id == orm.task_id
assert task.handler_name == orm.handler_name
assert task.owner_scope == orm.owner_scope
assert task.owner_id == orm.owner_id
assert task.schedule_kind == orm.schedule_kind
assert task.cron_expression == orm.cron_expression
assert task.run_at == orm.run_at
assert task.tz == orm.tz
assert task.payload == orm.payload
assert task.enabled == orm.enabled
assert task.delete_after_run == orm.delete_after_run
assert task.block_strategy == orm.block_strategy
assert task.stagger_seconds == orm.stagger_seconds
assert task.consecutive_errors == orm.consecutive_errors
assert task.status == orm.status
assert task.last_run_at == orm.last_run_at
assert task.next_run_at == orm.next_run_at
assert task.last_error == orm.last_error
assert task.created_by == orm.created_by
assert task.updated_by == orm.updated_by
assert task.created_at == orm.created_at
assert task.updated_at == orm.updated_at
assert task.is_deleted == orm.is_deleted
assert task.deleted_at == orm.deleted_at
# ─── orm_to_run_log ───────────────────────────────────────────────────────
@pytest.mark.unit
class TestOrmToRunLog:
def test_none_returns_none(self):
assert orm_to_run_log(None) is None
def test_basic_mapping(self):
# Arrange
orm = _make_run_log_orm()
# Act
log = orm_to_run_log(orm)
# Assert
assert isinstance(log, ScheduledTaskRunLog)
assert log.id == 1
assert log.task_id == "task-001"
assert log.run_id == "run-001"
assert log.triggered_by == "auto"
assert log.status == "success"
assert log.output == {"rows": 10}
assert log.started_at == datetime(2024, 1, 1, 0, 15, 0)
def test_all_fields_mapped(self):
orm = _make_run_log_orm()
log = orm_to_run_log(orm)
assert log.id == orm.id
assert log.task_id == orm.task_id
assert log.run_id == orm.run_id
assert log.triggered_by == orm.triggered_by
assert log.status == orm.status
assert log.error_message == orm.error_message
assert log.output == orm.output
assert log.started_at == orm.started_at
assert log.finished_at == orm.finished_at
assert log.created_by == orm.created_by
assert log.updated_by == orm.updated_by
assert log.created_at == orm.created_at
assert log.updated_at == orm.updated_at
assert log.is_deleted == orm.is_deleted
assert log.deleted_at == orm.deleted_at
# ─── daily_stat_row_to_dataclass ──────────────────────────────────────────
@pytest.mark.unit
class TestDailyStatRowToDataclass:
def test_basic_mapping(self):
# Arrange - row 可能是 NamedTuple 或 dataclass 实例
row = SimpleNamespace(
stat_date=datetime(2024, 1, 1).date(),
handler_name="h",
success_count=10,
failure_count=2,
timeout_count=1,
dead_letter_count=0,
)
# Act
stat = daily_stat_row_to_dataclass(row)
# Assert
assert isinstance(stat, DailyStat)
assert stat.stat_date == datetime(2024, 1, 1).date()
assert stat.handler_name == "h"
assert stat.success_count == 10
assert stat.failure_count == 2
assert stat.timeout_count == 1
assert stat.dead_letter_count == 0
def test_string_counts_converted_to_int(self):
# Arrange - 仓储层可能返回字符串计数值
row = SimpleNamespace(
stat_date=datetime(2024, 1, 1).date(),
handler_name="h",
success_count="10",
failure_count="2",
timeout_count="1",
dead_letter_count="0",
)
# Act
stat = daily_stat_row_to_dataclass(row)
# Assert - int() 转换
assert stat.success_count == 10
assert stat.failure_count == 2
assert stat.timeout_count == 1
assert stat.dead_letter_count == 0
assert isinstance(stat.success_count, int)
# ─── handler_summary_row_to_dataclass ─────────────────────────────────────
@pytest.mark.unit
class TestHandlerSummaryRowToDataclass:
def test_basic_mapping(self):
# Arrange
row = SimpleNamespace(
name="my_handler",
task_count=5,
last_active_at=datetime(2024, 1, 1, 0, 0, 0),
)
# Act
summary = handler_summary_row_to_dataclass(row)
# Assert
assert isinstance(summary, HandlerSummary)
assert summary.name == "my_handler"
assert summary.task_count == 5
assert summary.last_active_at == datetime(2024, 1, 1, 0, 0, 0)
def test_string_task_count_converted_to_int(self):
# Arrange
row = SimpleNamespace(
name="h",
task_count="7",
last_active_at=None,
)
# Act
summary = handler_summary_row_to_dataclass(row)
# Assert
assert summary.task_count == 7
assert isinstance(summary.task_count, int)
def test_none_last_active_at(self):
row = SimpleNamespace(name="h", task_count=0, last_active_at=None)
summary = handler_summary_row_to_dataclass(row)
assert summary.last_active_at is None