Spaces:
Sleeping
Sleeping
Download tests/test_api_detect.py from Moni38/smartvision-ai: direct link, hf CLI and curl.
- Browser
- Download file 9.83 kB
-
https://huggingface.co/spaces/Moni38/smartvision-ai/resolve/main/tests/test_api_detect.py
- Command line
-
hf download hf://spaces/Moni38/smartvision-ai/tests/test_api_detect.py
-
curl -L -o test_api_detect.py https://huggingface.co/spaces/Moni38/smartvision-ai/resolve/main/tests/test_api_detect.py
9.83 kB
| """ | |
| tests/test_api_detect.py | |
| ββββββββββββββββββββββββ | |
| Tests for POST /detect endpoint (10 tests). | |
| YOLO mock architecture | |
| ----------------------- | |
| detect.py calls: | |
| results = yolo.predict(img_array, conf=conf_threshold, verbose=False) | |
| for box in results[0].boxes: | |
| box.cls[0].item() β class index (int) | |
| box.conf[0].item() β confidence (float) | |
| box.xyxy[0][i].item() β bbox coordinates (float) | |
| MockYOLOBox provides real torch.Tensor values so .item() works correctly. | |
| The mock YOLO model returns a list with one Results-like object. | |
| Conf-threshold filtering is performed by YOLO internally; the endpoint | |
| passes conf_threshold directly to yolo.predict and does NOT re-filter. | |
| Tests verify the conf_threshold is forwarded correctly. | |
| """ | |
| from __future__ import annotations | |
| from typing import TYPE_CHECKING | |
| from unittest.mock import MagicMock | |
| import pytest | |
| import torch | |
| from api.schemas import DetectResponse | |
| if TYPE_CHECKING: | |
| from fastapi.testclient import TestClient | |
| # ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MockYOLOBox: | |
| """Minimal YOLO box with real tensors so .item() works.""" | |
| def __init__( | |
| self, | |
| class_id: int = 0, | |
| conf: float = 0.92, | |
| x1: float = 10.0, | |
| y1: float = 20.0, | |
| x2: float = 50.0, | |
| y2: float = 80.0, | |
| ): | |
| self.cls = [torch.tensor(float(class_id))] | |
| self.conf = [torch.tensor(conf)] | |
| self.xyxy = [torch.tensor([x1, y1, x2, y2])] | |
| def _make_yolo_mock(boxes: list[MockYOLOBox] | None = None) -> MagicMock: | |
| """Build a YOLO model mock returning the given boxes.""" | |
| if boxes is None: | |
| boxes = [] | |
| result = MagicMock() | |
| result.names = {0: "person", 1: "bicycle", 2: "car"} | |
| result.boxes = boxes | |
| yolo = MagicMock() | |
| yolo.predict.return_value = [result] | |
| return yolo | |
| def _post_detect( | |
| client: "TestClient", | |
| image_bytes: bytes, | |
| conf_threshold: float = 0.5, | |
| ) -> "TestClient": | |
| return client.post( | |
| "/detect", | |
| files={"file": ("test.jpg", image_bytes, "image/jpeg")}, | |
| data={"conf_threshold": conf_threshold}, | |
| ) | |
| # ββ Cache hit / miss βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_detect_cache_miss_returns_cached_false( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| """Cache miss: YOLO inference runs, cached=False.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| app_client.app.state.redis.get.return_value = None | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| assert response.json()["cached"] is False | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| def test_detect_cache_hit_returns_cached_true( | |
| app_client: "TestClient", | |
| test_image_bytes: bytes, | |
| mock_detect_response: dict, | |
| ) -> None: | |
| """Cache hit: stored payload returned with cached=True, YOLO not called.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| app_client.app.state.redis.get.return_value = mock_detect_response | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| assert response.json()["cached"] is True | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| # ββ Confidence threshold forwarded to YOLO ββββββββββββββββββββββββββββββββββββ | |
| def test_detect_conf_threshold_forwarded_to_yolo( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| """Endpoint passes conf_threshold to yolo.predict (YOLO handles filtering).""" | |
| mock_yolo = _make_yolo_mock() | |
| app_client.app.state.models["yolo"] = mock_yolo | |
| app_client.app.state.redis.get.return_value = None | |
| try: | |
| _post_detect(app_client, test_image_bytes, conf_threshold=0.9) | |
| call_kwargs = mock_yolo.predict.call_args[1] | |
| assert call_kwargs["conf"] == pytest.approx(0.9) | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| def test_detect_empty_detections_when_yolo_returns_none( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| """YOLO returns zero boxes β detections list is empty.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock(boxes=[]) | |
| app_client.app.state.redis.get.return_value = None | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| assert response.json()["detections"] == [] | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| # ββ 503 error conditions βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_detect_503_when_models_not_ready( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| app_client.app.state.models_ready = False | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 503 | |
| finally: | |
| app_client.app.state.models_ready = True | |
| def test_detect_503_when_yolo_model_missing( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| """Default app_client has resnet50 + mobilenet but NO yolo β 503.""" | |
| assert "yolo" not in app_client.app.state.models | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 503 | |
| assert "detection model" in response.json()["detail"].lower() | |
| # ββ Bbox coordinate correctness ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_detect_bbox_x1_lt_x2_and_y1_lt_y2( | |
| app_client: "TestClient", | |
| test_image_bytes: bytes, | |
| mock_detect_response: dict, | |
| ) -> None: | |
| """All returned bboxes satisfy x1 < x2 and y1 < y2.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| app_client.app.state.redis.get.return_value = mock_detect_response | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| for det in response.json()["detections"]: | |
| assert det["x1"] < det["x2"], f"x1 >= x2: {det}" | |
| assert det["y1"] < det["y2"], f"y1 >= y2: {det}" | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| # ββ Response body correctness ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_detect_inference_time_is_positive( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| app_client.app.state.redis.get.return_value = None | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| assert response.json()["inference_time_ms"] > 0 | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| def test_detect_response_validates_schema( | |
| app_client: "TestClient", | |
| test_image_bytes: bytes, | |
| mock_detect_response: dict, | |
| ) -> None: | |
| """Response dict validates against DetectResponse Pydantic schema.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| app_client.app.state.redis.get.return_value = mock_detect_response | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| parsed = DetectResponse(**response.json()) | |
| assert parsed.cached is True | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| # ββ Input validation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_detect_non_image_bytes_rejected(app_client: "TestClient") -> None: | |
| """Garbage bytes that PIL cannot open β error before YOLO is called.""" | |
| app_client.app.state.models["yolo"] = _make_yolo_mock() | |
| try: | |
| response = app_client.post( | |
| "/detect", | |
| files={"file": ("bad.jpg", b"not an image", "image/jpeg")}, | |
| data={"conf_threshold": 0.5}, | |
| ) | |
| assert response.status_code in (400, 422, 500) | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |
| def test_detect_real_box_coordinates_extracted_correctly( | |
| app_client: "TestClient", test_image_bytes: bytes | |
| ) -> None: | |
| """When YOLO returns a real box, coordinates appear correctly in response.""" | |
| box = MockYOLOBox(class_id=0, conf=0.88, x1=5.0, y1=10.0, x2=100.0, y2=200.0) | |
| mock_yolo = _make_yolo_mock(boxes=[box]) | |
| app_client.app.state.models["yolo"] = mock_yolo | |
| app_client.app.state.redis.get.return_value = None | |
| try: | |
| response = _post_detect(app_client, test_image_bytes) | |
| assert response.status_code == 200 | |
| dets = response.json()["detections"] | |
| assert len(dets) == 1 | |
| d = dets[0] | |
| assert d["class_name"] == "person" # names[0] = "person" | |
| assert d["confidence"] == pytest.approx(0.88, abs=1e-4) | |
| assert d["x1"] == pytest.approx(5.0) | |
| assert d["y1"] == pytest.approx(10.0) | |
| assert d["x2"] == pytest.approx(100.0) | |
| assert d["y2"] == pytest.approx(200.0) | |
| finally: | |
| app_client.app.state.models.pop("yolo", None) | |