mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 12:06:32 +00:00
86b8f076b7
- Modified the health endpoint to return "None" for AI availability when inference is not initialized, improving clarity on system status. - Enhanced the test documentation to include handling of skipped tests, emphasizing the need for investigation before proceeding. - Updated test assertions to ensure proper execution order and prevent premature engine initialization. - Refactored test cases to streamline performance testing and improve readability, removing unnecessary complexity. These changes aim to enhance the robustness of the health check and improve the overall testing framework.
224 lines
5.7 KiB
Python
224 lines
5.7 KiB
Python
import base64
|
|
import json
|
|
import os
|
|
import random
|
|
import time
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import requests
|
|
import sseclient
|
|
from pytest import ExitCode
|
|
|
|
|
|
def pytest_collection_modifyitems(items):
|
|
early = []
|
|
rest = []
|
|
for item in items:
|
|
if "Step01PreInit" in item.nodeid or "Step02LazyInit" in item.nodeid:
|
|
early.append(item)
|
|
else:
|
|
rest.append(item)
|
|
items[:] = early + rest
|
|
|
|
|
|
@pytest.hookimpl(trylast=True)
|
|
def pytest_sessionfinish(session, exitstatus):
|
|
if exitstatus in (ExitCode.NO_TESTS_COLLECTED, 5):
|
|
session.exitstatus = ExitCode.OK
|
|
|
|
|
|
class _SessionWithBase(requests.Session):
|
|
def __init__(self, base: str, default_timeout: float = 30):
|
|
super().__init__()
|
|
self._base = base.rstrip("/")
|
|
self._default_timeout = default_timeout
|
|
|
|
def request(self, method, url, **kwargs):
|
|
if url.startswith("http://") or url.startswith("https://"):
|
|
full = url
|
|
else:
|
|
path = url if url.startswith("/") else f"/{url}"
|
|
full = f"{self._base}{path}"
|
|
kwargs.setdefault("timeout", self._default_timeout)
|
|
return super().request(method, full, **kwargs)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def base_url():
|
|
return os.environ.get("BASE_URL", "http://detections:8080")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def http_client(base_url):
|
|
return _SessionWithBase(base_url, 30)
|
|
|
|
|
|
@pytest.fixture
|
|
def sse_client_factory(http_client):
|
|
@contextmanager
|
|
def _open():
|
|
with http_client.get("/detect/stream", stream=True, timeout=600) as resp:
|
|
resp.raise_for_status()
|
|
yield sseclient.SSEClient(resp)
|
|
|
|
return _open
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def mock_loader_url():
|
|
return os.environ.get("MOCK_LOADER_URL", "http://mock-loader:8080")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def mock_annotations_url():
|
|
return os.environ.get("MOCK_ANNOTATIONS_URL", "http://mock-annotations:8081")
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def wait_for_services(base_url, mock_loader_url, mock_annotations_url):
|
|
urls = [
|
|
f"{base_url}/health",
|
|
f"{mock_loader_url}/mock/status",
|
|
f"{mock_annotations_url}/mock/status",
|
|
]
|
|
deadline = time.time() + 120
|
|
while time.time() < deadline:
|
|
ok = True
|
|
for u in urls:
|
|
try:
|
|
r = requests.get(u, timeout=5)
|
|
if r.status_code != 200:
|
|
ok = False
|
|
break
|
|
except OSError:
|
|
ok = False
|
|
break
|
|
if ok:
|
|
return
|
|
time.sleep(2)
|
|
pytest.fail("services not ready within 120s")
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_mocks(mock_loader_url, mock_annotations_url):
|
|
requests.post(f"{mock_loader_url}/mock/reset", timeout=10)
|
|
requests.post(f"{mock_annotations_url}/mock/reset", timeout=10)
|
|
yield
|
|
|
|
|
|
def _media_dir() -> Path:
|
|
return Path(os.environ.get("MEDIA_DIR", "/media"))
|
|
|
|
|
|
def _read_media(name: str) -> bytes:
|
|
p = _media_dir() / name
|
|
if not p.is_file():
|
|
pytest.skip(f"missing {p}")
|
|
return p.read_bytes()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def media_dir():
|
|
return str(_media_dir())
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_small():
|
|
return _read_media("image_small.jpg")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_large():
|
|
return _read_media("image_large.JPG")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_dense():
|
|
return _read_media("image_dense01.jpg")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_dense_02():
|
|
return _read_media("image_dense02.jpg")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_different_types():
|
|
return _read_media("image_different_types.jpg")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def image_empty_scene():
|
|
return _read_media("image_empty_scene.jpg")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def video_short_path():
|
|
return str(_media_dir() / "video_test01.mp4")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def video_short_02_path():
|
|
return str(_media_dir() / "video_short02.mp4")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def video_long_path():
|
|
return str(_media_dir() / "video_long03.mp4")
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def empty_image():
|
|
return b""
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def corrupt_image():
|
|
random.seed(42)
|
|
return random.randbytes(1024)
|
|
|
|
|
|
def _b64url_obj(obj: dict) -> str:
|
|
raw = json.dumps(obj, separators=(",", ":")).encode()
|
|
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
|
|
|
|
|
@pytest.fixture
|
|
def jwt_token():
|
|
header = (
|
|
base64.urlsafe_b64encode(json.dumps({"alg": "none", "typ": "JWT"}).encode())
|
|
.decode()
|
|
.rstrip("=")
|
|
)
|
|
payload = _b64url_obj({"exp": int(time.time()) + 3600, "sub": "test"})
|
|
return f"{header}.{payload}.signature"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def warm_engine(http_client, image_small):
|
|
deadline = time.time() + 120
|
|
files = {"file": ("warm.jpg", image_small, "image/jpeg")}
|
|
consecutive_errors = 0
|
|
last_status = None
|
|
while time.time() < deadline:
|
|
try:
|
|
r = http_client.post("/detect", files=files)
|
|
if r.status_code == 200:
|
|
return
|
|
last_status = r.status_code
|
|
if r.status_code >= 500:
|
|
consecutive_errors += 1
|
|
if consecutive_errors >= 5:
|
|
pytest.fail(
|
|
f"engine warm-up aborted: {consecutive_errors} consecutive "
|
|
f"HTTP {last_status} errors — server is broken, not starting up"
|
|
)
|
|
else:
|
|
consecutive_errors = 0
|
|
except OSError:
|
|
consecutive_errors = 0
|
|
time.sleep(2)
|
|
pytest.fail(f"engine warm-up timed out after 120s (last status: {last_status})")
|