101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
|
|
"""排队任务要有实际执行者;忙锁、断连和进程中断不能被误记成成功。"""
|
||
|
|
|
||
|
|
from contextlib import contextmanager, nullcontext
|
||
|
|
from unittest.mock import Mock
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
from sqlalchemy import select
|
||
|
|
|
||
|
|
from app.core.tplus_lock import TPlusBusyError
|
||
|
|
from app.models import TPlusSyncError
|
||
|
|
from app.schemas.tplus import TPlusCreateIn
|
||
|
|
from app.services.tplus import task_service, worker
|
||
|
|
|
||
|
|
|
||
|
|
def _queued(session, audit=False):
|
||
|
|
return task_service.create_task(
|
||
|
|
session,
|
||
|
|
TPlusCreateIn(
|
||
|
|
request_key=uuid4(),
|
||
|
|
task_type="AUDIT" if audit else "READ",
|
||
|
|
entity_types=[] if audit else ["products"],
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _worker_setup(monkeypatch):
|
||
|
|
monkeypatch.setattr(worker, "sync_lock", lambda session: nullcontext())
|
||
|
|
monkeypatch.setattr(
|
||
|
|
"app.services.tplus.reader.TPlusReader.iter_entity",
|
||
|
|
lambda *a, **kw: iter([{"id": 1, "code": "A"}]),
|
||
|
|
)
|
||
|
|
return lambda: nullcontext(Mock())
|
||
|
|
|
||
|
|
|
||
|
|
def test_读取任务由worker处理并可观察重复数(tplus_session, monkeypatch):
|
||
|
|
source = _worker_setup(monkeypatch)
|
||
|
|
first = _queued(tplus_session)
|
||
|
|
assert worker.run_once(tplus_session, source_factory=source) == first.batch_no
|
||
|
|
assert first.status == "SUCCEEDED" and first.started_at is not None
|
||
|
|
second = _queued(tplus_session)
|
||
|
|
worker.run_once(tplus_session, source_factory=source)
|
||
|
|
assert second.staged_record_count == 0
|
||
|
|
assert second.skipped_record_count == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_忙锁时不领取任务也不恢复其他运行记录(tplus_session, monkeypatch):
|
||
|
|
source = _worker_setup(monkeypatch)
|
||
|
|
batch = _queued(tplus_session)
|
||
|
|
|
||
|
|
@contextmanager
|
||
|
|
def busy(_session):
|
||
|
|
raise TPlusBusyError("已有任务")
|
||
|
|
yield
|
||
|
|
|
||
|
|
monkeypatch.setattr(worker, "sync_lock", busy)
|
||
|
|
assert worker.run_once(tplus_session, source_factory=source) is None
|
||
|
|
assert batch.status == "QUEUED"
|
||
|
|
|
||
|
|
|
||
|
|
def test_源连接失败写入固定错误不泄漏凭据(tplus_session, monkeypatch):
|
||
|
|
_worker_setup(monkeypatch)
|
||
|
|
batch = _queued(tplus_session)
|
||
|
|
|
||
|
|
def unavailable():
|
||
|
|
raise RuntimeError("password=private-secret")
|
||
|
|
|
||
|
|
worker.run_once(tplus_session, source_factory=unavailable)
|
||
|
|
assert batch.status == "FAILED"
|
||
|
|
error = tplus_session.scalar(select(TPlusSyncError))
|
||
|
|
assert "private-secret" not in error.error_message
|
||
|
|
assert error.error_code == "EXECUTION_ERROR"
|
||
|
|
|
||
|
|
|
||
|
|
def test_取得锁后把遗留运行中任务记为中断失败(tplus_session, monkeypatch):
|
||
|
|
source = _worker_setup(monkeypatch)
|
||
|
|
batch = _queued(tplus_session)
|
||
|
|
batch.status = "RUNNING"
|
||
|
|
tplus_session.commit()
|
||
|
|
assert worker.run_once(tplus_session, source_factory=source) is None
|
||
|
|
assert batch.status == "FAILED"
|
||
|
|
assert tplus_session.scalar(select(TPlusSyncError.error_code)) == "INTERRUPTED"
|
||
|
|
|
||
|
|
|
||
|
|
def test_核验任务持久化报告但不修改业务数据(tplus_session, monkeypatch):
|
||
|
|
source = _worker_setup(monkeypatch)
|
||
|
|
batch = _queued(tplus_session, audit=True)
|
||
|
|
monkeypatch.setattr(
|
||
|
|
worker.TPlusRelationshipAudit,
|
||
|
|
"run",
|
||
|
|
lambda self: {
|
||
|
|
"relationship_status": "OBSERVED",
|
||
|
|
"receipt_kinds": [{"row_count": 10}],
|
||
|
|
"compatibility_status": "PENDING",
|
||
|
|
"is_target_engine": False,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
worker.run_once(tplus_session, source_factory=source)
|
||
|
|
assert batch.status == "SUCCEEDED"
|
||
|
|
assert batch.audit_result_json["compatibility_status"] == "PENDING"
|
||
|
|
assert batch.staged_record_count == 0
|