mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 21:56:33 +00:00
fc57d677b4
- Updated various Cython files to explicitly cast types, enhancing type safety and readability. - Adjusted the `engine_name` property in `InferenceEngine` and its subclasses to be set directly in the constructor. - Modified the `request` method in `_SessionWithBase` to accept `*args` for better flexibility. - Ensured proper type casting for return values in methods across multiple classes, including `Inference`, `CoreMLEngine`, and `TensorRTEngine`. These changes aim to streamline the codebase and improve maintainability by enforcing consistent type usage.
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, *args, **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, *args, **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})")
|