Initial curated ServiceMap source from 7a0a61232d05ba9097432abef6f7f4102a64d0b8
This commit is contained in:
commit
105251b9c8
96 files changed
+23587
No files matched your search
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
from typing import Self
|
||||
|
||||
import pytest
|
||||
|
||||
from app import probes
|
||||
from app.models import HealthStatus, ProbeResult, Service, ServiceKind
|
||||
|
||||
|
||||
def _service() -> Service:
|
||||
now = datetime.now(UTC)
|
||||
return Service(
|
||||
id="service-1",
|
||||
name="API",
|
||||
kind=ServiceKind.HTTP,
|
||||
url="https://api.local",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
|
||||
def _tcp_service() -> Service:
|
||||
now = datetime.now(UTC)
|
||||
return Service(
|
||||
id="tcp-service-1",
|
||||
name="TCP service",
|
||||
kind=ServiceKind.TCP,
|
||||
host="tcp.local",
|
||||
port=5432,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "expected"),
|
||||
[
|
||||
(200, HealthStatus.HEALTHY),
|
||||
(302, HealthStatus.HEALTHY),
|
||||
(404, HealthStatus.DEGRADED),
|
||||
(503, HealthStatus.DOWN),
|
||||
],
|
||||
)
|
||||
def test_http_status_classification(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
status_code: int,
|
||||
expected: HealthStatus,
|
||||
) -> None:
|
||||
class FakeResponse:
|
||||
def __init__(self, code: int) -> None:
|
||||
self.status_code = code
|
||||
|
||||
class FakeClient:
|
||||
async def __aenter__(self) -> Self:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
async def get(self, url: str) -> FakeResponse:
|
||||
assert url == "https://api.local"
|
||||
return FakeResponse(status_code)
|
||||
|
||||
monkeypatch.setattr(probes.httpx, "AsyncClient", lambda **kwargs: FakeClient())
|
||||
|
||||
status, detail = asyncio.run(probes._probe_once(_service(), 1.0))
|
||||
|
||||
assert status == expected
|
||||
assert detail == f"HTTP {status_code}"
|
||||
|
||||
|
||||
def test_retry_latency_reports_final_attempt_not_backoff(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
calls = 0
|
||||
|
||||
async def fake_probe_once(service: Service, timeout_seconds: float) -> tuple[HealthStatus, str]:
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
if calls == 1:
|
||||
raise OSError("temporary failure")
|
||||
return HealthStatus.HEALTHY, "HTTP 200"
|
||||
|
||||
timings = iter([10.0, 10.05, 20.0, 20.02])
|
||||
monkeypatch.setattr(probes, "_probe_once", fake_probe_once)
|
||||
monkeypatch.setattr(probes, "perf_counter", lambda: next(timings))
|
||||
|
||||
result = asyncio.run(probes.probe_service(_service(), timeout_seconds=1.0, retries=1))
|
||||
|
||||
assert calls == 2
|
||||
assert result.status == HealthStatus.HEALTHY
|
||||
assert result.latency_ms == pytest.approx(20.0)
|
||||
assert result.detail == "HTTP 200 after 2 attempts"
|
||||
|
||||
|
||||
def test_tcp_probe_success_and_failure(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class FakeWriter:
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
async def wait_closed(self) -> None:
|
||||
return None
|
||||
|
||||
async def fake_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]:
|
||||
assert (host, port) == ("tcp.local", 5432)
|
||||
return object(), FakeWriter()
|
||||
|
||||
monkeypatch.setattr(probes.asyncio, "open_connection", fake_open_connection)
|
||||
status, detail = asyncio.run(probes._probe_once(_tcp_service(), 1.0))
|
||||
assert status == HealthStatus.HEALTHY
|
||||
assert detail == "TCP tcp.local:5432 reachable"
|
||||
|
||||
async def failing_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]:
|
||||
raise ConnectionRefusedError("refused")
|
||||
|
||||
monkeypatch.setattr(probes.asyncio, "open_connection", failing_open_connection)
|
||||
result = asyncio.run(probes.probe_service(_tcp_service(), timeout_seconds=1.0, retries=0))
|
||||
assert result.status == HealthStatus.DOWN
|
||||
assert "ConnectionRefusedError" in result.detail
|
||||
|
||||
|
||||
def test_probe_many_respects_concurrency_boundary(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
services = [
|
||||
_service().model_copy(update={"id": f"service-{index}", "name": f"API-{index}"})
|
||||
for index in range(5)
|
||||
]
|
||||
active = 0
|
||||
maximum = 0
|
||||
|
||||
async def fake_probe_service(
|
||||
service: Service,
|
||||
*,
|
||||
timeout_seconds: float,
|
||||
retries: int,
|
||||
) -> ProbeResult:
|
||||
nonlocal active, maximum
|
||||
active += 1
|
||||
maximum = max(maximum, active)
|
||||
await asyncio.sleep(0)
|
||||
active -= 1
|
||||
return ProbeResult(
|
||||
id=f"probe-{service.id}",
|
||||
service_id=service.id,
|
||||
status=HealthStatus.HEALTHY,
|
||||
latency_ms=1.0,
|
||||
detail="ok",
|
||||
checked_at=datetime.now(UTC),
|
||||
)
|
||||
|
||||
monkeypatch.setattr(probes, "probe_service", fake_probe_service)
|
||||
|
||||
results = asyncio.run(probes.probe_many(services, concurrency=2))
|
||||
|
||||
assert len(results) == len(services)
|
||||
assert maximum <= 2
|
||||
Reference in new issue
Block a user