ForcePilot/backend/test/unit/scheduler/adapters/persistence/test_mappers.py
Kris 08617091dc refactor: 整理项目包结构与导入路径
- 新增多个业务域的__init__.py模块文件,规范包导出结构
- 调整多个DTO文件的导入路径,统一模块组织方式
- 移除测试文件中多余的空行与导入语句
- 优化部分业务模块的包层级划分
2026-07-18 02:04:03 +08:00

309 lines
11 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.

"""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",
"handler_name": "my_handler",
"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,
active_task_count=3,
dead_letter_count=1,
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.active_task_count == 3
assert summary.dead_letter_count == 1
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",
active_task_count="3",
dead_letter_count="1",
last_active_at=None,
)
# Act
summary = handler_summary_row_to_dataclass(row)
# Assert
assert summary.task_count == 7
assert isinstance(summary.task_count, int)
assert summary.active_task_count == 3
assert summary.dead_letter_count == 1
def test_none_last_active_at(self):
row = SimpleNamespace(
name="h",
task_count=0,
active_task_count=0,
dead_letter_count=0,
last_active_at=None,
)
summary = handler_summary_row_to_dataclass(row)
assert summary.last_active_at is None
def test_dead_letter_count_defaults_zero_when_missing(self):
# Arrange - 仓储层未返回 dead_letter_count 时兜底为 0
row = SimpleNamespace(name="h", task_count=0, last_active_at=None)
# Act
summary = handler_summary_row_to_dataclass(row)
# Assert
assert summary.dead_letter_count == 0
assert summary.active_task_count == 0