139 lines
5.0 KiB
Python
139 lines
5.0 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")
|
||
|
||
SUBJECTS = {"image_request": "files.img_uploaded", "result": "files.tabular_parsed"}
|
||
|
||
|
||
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), subjects=SUBJECTS)
|
||
assert await health.snapshot() == {"nats": "disabled", "hotfolder": "ok", "subjects": SUBJECTS}
|
||
|
||
|
||
async def test_missing_root_is_down(tmp_path: Path) -> None:
|
||
health = HealthProvider(nats_enabled=False, hotfolder=LocalHotFolder(tmp_path / "missing"), subjects=SUBJECTS)
|
||
assert await health.hotfolder_status() == "down"
|
||
|
||
|
||
async def test_no_hotfolder_is_down() -> None:
|
||
assert await HealthProvider(nats_enabled=False, hotfolder=None, subjects=SUBJECTS).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, subjects=SUBJECTS)
|
||
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, subjects=SUBJECTS)
|
||
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, subjects=SUBJECTS
|
||
)
|
||
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, subjects=SUBJECTS)
|
||
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), subjects=SUBJECTS)
|
||
health.nats = Connection(True)
|
||
assert await health.snapshot() == {"nats": "ok", "hotfolder": "ok", "subjects": SUBJECTS}
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("enabled", "connection", "expected"),
|
||
[(True, Connection(True), "ok"), (True, Connection(False), "down"), (False, None, "disabled")],
|
||
)
|
||
async def test_snapshot_contains_subjects(enabled: bool, connection: Connection | None, expected: str) -> None:
|
||
health = HealthProvider(nats_enabled=enabled, hotfolder=None, subjects=SUBJECTS)
|
||
health.nats = connection
|
||
|
||
snapshot = await health.snapshot()
|
||
|
||
assert snapshot["nats"] == expected
|
||
assert snapshot["subjects"] == SUBJECTS
|