- HotFolder.poll(job_id) -> list[AnswerFile] | None: все *.json каталога <output>/<job_id>/ по возрастанию имени, когда набор не пуст и имена/размеры не менялись SCAN_SETTLE_SECONDS (SettleTracker: состояние по job_id, внедряемые часы, сброс при выдаче/опустении, предел 1024 задания, потокобезопасно); - put: временный .<name>.part в <корень>/<SCAN_TMP_DIR>/ (создаётся), rename в Input (Local и SMB); - настройки SCAN_TMP_DIR (Tmp) и SCAN_SETTLE_SECONDS (2, >= 0); в .env.test settle = 0 (R-23); - воркер: переходная прослойка — берётся первый файл набора (склейка всех — Task 3, R-22). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
121 lines
4.2 KiB
Python
121 lines
4.2 KiB
Python
"""Состояние для `/healthcheck` (`src.core.health`): поля `nats` и `hotfolder` (spec «Старт в режиме локальных папок»,
|
||
«Горячая папка недоступна», «Соединение с NATS без JetStream»; D-8; T8.4 — проверка не зависает, T2.3 — сбой SMB)."""
|
||
|
||
import threading
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from src.core.health import HealthProvider
|
||
from src.gateway.hotfolder import AnswerFile, LocalHotFolder
|
||
|
||
pytestmark = pytest.mark.usefixtures("log_lines")
|
||
|
||
|
||
class Connection:
|
||
def __init__(self, connected: bool) -> None:
|
||
self.is_connected = connected
|
||
|
||
|
||
class CountingHotFolder:
|
||
"""`healthy()` считает вызовы; может зависнуть до события или бросить исключение."""
|
||
|
||
def __init__(self, result: bool = True) -> None:
|
||
self.result = result
|
||
self.calls = 0
|
||
self.release: threading.Event | None = None
|
||
self.error: BaseException | None = None
|
||
|
||
def put(self, name: str, data: bytes) -> None:
|
||
raise NotImplementedError
|
||
|
||
def poll(self, job_id: str) -> list[AnswerFile] | None:
|
||
raise NotImplementedError
|
||
|
||
def healthy(self) -> bool:
|
||
self.calls += 1
|
||
if self.release is not None:
|
||
self.release.wait(5)
|
||
if self.error is not None:
|
||
raise self.error
|
||
return self.result
|
||
|
||
|
||
class Clock:
|
||
def __init__(self) -> None:
|
||
self.now = 1000.0
|
||
|
||
def __call__(self) -> float:
|
||
return self.now
|
||
|
||
|
||
async def test_local_root_exists_is_ok(tmp_path: Path) -> None:
|
||
health = HealthProvider(nats_enabled=False, hotfolder=LocalHotFolder(tmp_path))
|
||
assert await health.snapshot() == {"nats": "disabled", "hotfolder": "ok"}
|
||
|
||
|
||
async def test_missing_root_is_down(tmp_path: Path) -> None:
|
||
health = HealthProvider(nats_enabled=False, hotfolder=LocalHotFolder(tmp_path / "missing"))
|
||
assert await health.hotfolder_status() == "down"
|
||
|
||
|
||
async def test_no_hotfolder_is_down() -> None:
|
||
assert await HealthProvider(nats_enabled=False, hotfolder=None).hotfolder_status() == "down"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("enabled", "connection", "expected"),
|
||
[
|
||
(False, None, "disabled"),
|
||
(False, Connection(True), "disabled"),
|
||
(True, None, "down"),
|
||
(True, Connection(False), "down"),
|
||
(True, Connection(True), "ok"),
|
||
],
|
||
)
|
||
def test_nats_status(enabled: bool, connection: Connection | None, expected: str) -> None:
|
||
health = HealthProvider(nats_enabled=enabled, hotfolder=None)
|
||
health.nats = connection
|
||
assert health.nats_status() == expected
|
||
|
||
|
||
async def test_hotfolder_result_is_cached_for_ttl() -> None:
|
||
hotfolder = CountingHotFolder(result=True)
|
||
clock = Clock()
|
||
health = HealthProvider(nats_enabled=False, hotfolder=hotfolder, cache_ttl=5, clock=clock)
|
||
assert await health.hotfolder_status() == "ok"
|
||
hotfolder.result = False
|
||
clock.now += 4.9
|
||
assert await health.hotfolder_status() == "ok"
|
||
assert hotfolder.calls == 1
|
||
clock.now += 0.2
|
||
assert await health.hotfolder_status() == "down"
|
||
assert hotfolder.calls == 2
|
||
|
||
|
||
async def test_hanging_check_is_down_within_timeout_and_not_repeated() -> None:
|
||
hotfolder = CountingHotFolder()
|
||
hotfolder.release = threading.Event()
|
||
clock = Clock()
|
||
health = HealthProvider(nats_enabled=False, hotfolder=hotfolder, check_timeout=0.05, cache_ttl=5, clock=clock)
|
||
try:
|
||
assert await health.hotfolder_status() == "down"
|
||
clock.now += 10 # кэш истёк, но прежняя проверка ещё висит — новый поток не запускается
|
||
assert await health.hotfolder_status() == "down"
|
||
assert hotfolder.calls == 1
|
||
finally:
|
||
hotfolder.release.set()
|
||
|
||
|
||
async def test_check_raising_is_down() -> None:
|
||
hotfolder = CountingHotFolder()
|
||
hotfolder.error = OSError("smb")
|
||
health = HealthProvider(nats_enabled=False, hotfolder=hotfolder)
|
||
assert await health.hotfolder_status() == "down"
|
||
|
||
|
||
async def test_snapshot_with_connected_nats(tmp_path: Path) -> None:
|
||
health = HealthProvider(nats_enabled=True, hotfolder=LocalHotFolder(tmp_path))
|
||
health.nats = Connection(True)
|
||
assert await health.snapshot() == {"nats": "ok", "hotfolder": "ok"}
|