PayrollManagementSystem/backend/tests/unit/test_tplus_automation.py

161 lines
7.1 KiB
Python
Raw Normal View History

"""周期和自动重试只能生成原始采集,验证重启防重与人工介入边界。"""
from datetime import datetime, timedelta
import pytest
from sqlalchemy import func, select
from app.core.config import Settings
from app.models import TPlusAutomationState, TPlusSyncBatch, TPlusSyncError
from app.services.tplus import automation
from tests.unit.test_tplus_workbench import work_session as work_session
NOW = datetime(2026, 10, 30, 10)
@pytest.fixture
def settings():
return Settings(_env_file=None, app_secret_key="x" * 32, t_plus_enabled=True,
t_plus_database="test_source", t_plus_user="reader", t_plus_odbc_driver="unit",
t_plus_automation_enabled=True)
def run(s, settings, now=NOW):
return automation.tick(s, settings=settings, now=now)
def fail(s, number, *, status="FAILED", code="SOURCE_TIMEOUT", at=NOW):
row = s.scalar(select(TPlusSyncBatch).where(TPlusSyncBatch.batch_no == number))
row.status, row.finished_at = status, at
s.add(TPlusSyncError(batch_no=number, entity_type="system", source_table="system", error_code=code, error_message="固定错误说明"))
s.commit()
def test_默认关闭不会排队(work_session, settings):
result = run(work_session, settings.model_copy(update={"t_plus_automation_enabled": False}))
assert result == {"status": "DISABLED", "queued": False}
assert work_session.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 0
def test_同周期重复检查和重启只有一次九类采集(work_session, settings):
s = work_session
first = run(s, settings)
for _ in range(3):
assert not run(s, settings)["queued"]
batch = s.scalar(select(TPlusSyncBatch))
assert batch.batch_no == first["batch_no"] and batch.sync_mode == "FULL"
assert len(batch.entity_types_json) == 9 and batch.trigger_type == "SCHEDULED"
assert s.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 1
def test_停机多周只补最近一次(work_session, settings):
s = work_session
first = run(s, settings)
batch = s.scalar(select(TPlusSyncBatch))
batch.status, batch.finished_at = "SUCCEEDED", NOW
s.commit()
later = run(s, settings, NOW + timedelta(weeks=4))
assert later["queued"] and later["period"] != first["period"]
assert s.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 2
def test_人工任务占用时不堆积周期任务(work_session, settings):
s = work_session
s.add(TPlusSyncBatch(batch_no="manual", sync_mode="FULL", status="RUNNING", source_database="test_source", entity_types_json=["personnel"]))
s.commit()
assert run(s, settings)["status"] == "DEFERRED"
assert s.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 1
def test_明确超时仅按五十五分钟最多重试两次(work_session, settings):
s = work_session
first = run(s, settings)
fail(s, first["batch_no"])
assert not run(s, settings, NOW + timedelta(minutes=4))["queued"]
second = run(s, settings, NOW + timedelta(minutes=5))
assert second["queued"]
fail(s, second["batch_no"], at=NOW + timedelta(minutes=5))
assert not run(s, settings, NOW + timedelta(minutes=19))["queued"]
third = run(s, settings, NOW + timedelta(minutes=20))
assert third["queued"]
fail(s, third["batch_no"], at=NOW + timedelta(minutes=21))
assert not run(s, settings, NOW + timedelta(hours=2))["queued"]
rows = list(s.scalars(select(TPlusSyncBatch).order_by(TPlusSyncBatch.id)))
assert [(r.trigger_type, r.automatic_attempt) for r in rows] == [("SCHEDULED", 0), ("AUTO_RETRY", 1), ("AUTO_RETRY", 2)]
assert rows[1].parent_batch_no == rows[0].batch_no and rows[2].parent_batch_no == rows[1].batch_no
@pytest.mark.parametrize("status,code", [("PARTIAL", "SOURCE_TIMEOUT"), ("FAILED", "SOURCE_CONFIG_ERROR"), ("FAILED", "STAGE_ERROR"), ("FAILED", "EXECUTION_ERROR")])
def test_部分失败或不明确错误不自动重试(work_session, settings, status, code):
s = work_session
first = run(s, settings)
fail(s, first["batch_no"], status=status, code=code)
assert not run(s, settings, NOW + timedelta(hours=2))["queued"]
assert s.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 1
def test_ISO年周和到期时间边界稳定():
assert automation.due_period(datetime(2027, 1, 1, 10))[0] == "2026-W53"
assert automation.due_period(datetime(2026, 11, 2, 1, 59))[0] == "2026-W44"
assert automation.due_period(datetime(2026, 11, 2, 2))[0] == "2026-W45"
def test_站内提示覆盖排队超时和无成功同步(work_session, settings):
s = work_session
run(s, settings)
batch = s.scalar(select(TPlusSyncBatch))
batch.create_time = NOW - timedelta(minutes=16)
s.commit()
result = automation.overview(s, settings=settings, now=NOW)
assert {r["code"] for r in result["alerts"]} >= {"QUEUE_OVERDUE", "SYNC_STALE", "WORKER_OFFLINE"}
def test_读取心跳不把当前采集写成工资(work_session, settings):
from sqlalchemy.orm import sessionmaker
s = work_session
automation.heartbeat(sessionmaker(s.get_bind()), settings, "owner", "RUNNING", now=NOW)
s.expire_all()
state = s.scalar(select(TPlusAutomationState))
assert state.heartbeat_at == NOW and state.worker_phase == "RUNNING"
assert s.scalar(select(func.count()).select_from(TPlusSyncBatch)) == 0
@pytest.mark.parametrize("status,code", [("SUCCEEDED", None), ("FAILED", "SOURCE_CONFIG_ERROR"), ("FAILED", "SOURCE_TIMEOUT")])
def test_人工重试结果接入周期且不重置自动次数(work_session, settings, monkeypatch, status, code):
from uuid import uuid4
from app.schemas.tplus import TPlusRetryIn
from app.services.tplus import task_service
monkeypatch.setattr(task_service, "get_settings", lambda: settings)
s = work_session
first = run(s, settings)
fail(s, first["batch_no"])
auto = run(s, settings, NOW + timedelta(minutes=5))
fail(s, auto["batch_no"], at=NOW + timedelta(minutes=5))
manual = task_service.retry_task(s, auto["batch_no"], TPlusRetryIn(request_key=uuid4()))
assert run(s, settings, NOW + timedelta(minutes=6))["status"] == "DEFERRED"
if code:
fail(s, manual.batch_no, status=status, code=code, at=NOW + timedelta(minutes=6))
else:
manual.status, manual.finished_at = status, NOW + timedelta(minutes=6)
s.commit()
outcome = run(s, settings, NOW + timedelta(minutes=30))
assert outcome["queued"] == (code == "SOURCE_TIMEOUT")
state = s.scalar(select(TPlusAutomationState))
assert state.retry_attempts == (2 if outcome["queued"] else 1)
if status == "SUCCEEDED":
assert state.last_success_at == manual.finished_at
if outcome["queued"]:
child = s.scalar(select(TPlusSyncBatch).where(TPlusSyncBatch.batch_no == outcome["batch_no"]))
assert child.parent_batch_no == manual.batch_no
def test_停止标记阻止周期排队且不报告进程离线(work_session, settings, tmp_path):
marker = tmp_path / "stop.requested"
marker.touch()
stopped = settings.model_copy(update={"t_plus_stop_file": str(marker)})
assert run(work_session, stopped)["status"] == "STOP_REQUESTED"
assert not automation.overview(work_session, settings=stopped, now=NOW)["alerts"]