mirror of
https://github.com/azaion/detections.git
synced 2026-04-22 11:26:33 +00:00
[AZ-180] Refactor detection event handling and improve SSE support
- Updated the detection image endpoint to require a channel ID for event streaming. - Introduced a new endpoint for streaming detection events, allowing clients to receive real-time updates. - Enhanced the internal buffering mechanism for detection events to manage multiple channels. - Refactored the inference module to support the new event handling structure. Made-with: Cursor
This commit is contained in:
+27
-17
@@ -4,6 +4,7 @@ import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import sseclient
|
||||
|
||||
|
||||
def _ai_config_video() -> dict:
|
||||
@@ -19,36 +20,43 @@ def test_ft_p08_immediate_async_response(
|
||||
):
|
||||
media_id = f"async-{uuid.uuid4().hex}"
|
||||
body = _ai_config_image()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
channel_id = str(uuid.uuid4())
|
||||
headers = {"Authorization": f"Bearer {jwt_token}", "X-Channel-Id": channel_id}
|
||||
t0 = time.monotonic()
|
||||
r = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
elapsed = time.monotonic() - t0
|
||||
assert elapsed < 2.0
|
||||
assert r.status_code == 200
|
||||
assert r.json() == {"status": "started", "mediaId": media_id}
|
||||
assert r.status_code == 202
|
||||
|
||||
|
||||
@pytest.mark.timeout(10)
|
||||
def test_ft_p09_sse_event_delivery(
|
||||
warm_engine, http_client, jwt_token, sse_client_factory
|
||||
warm_engine, http_client, jwt_token
|
||||
):
|
||||
media_id = f"sse-{uuid.uuid4().hex}"
|
||||
channel_id = str(uuid.uuid4())
|
||||
body = _ai_config_video()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
auth_header = {"Authorization": f"Bearer {jwt_token}"}
|
||||
post_headers = {**auth_header, "X-Channel-Id": channel_id}
|
||||
collected: list[dict] = []
|
||||
thread_exc: list[BaseException] = []
|
||||
first_event = threading.Event()
|
||||
connected = threading.Event()
|
||||
|
||||
def _listen():
|
||||
try:
|
||||
with sse_client_factory(media_id) as sse:
|
||||
time.sleep(0.3)
|
||||
for event in sse.events():
|
||||
with http_client.get(
|
||||
f"/detect/events/{channel_id}",
|
||||
stream=True,
|
||||
timeout=600,
|
||||
headers=auth_header,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
connected.set()
|
||||
for event in sseclient.SSEClient(resp).events():
|
||||
if not event.data or not str(event.data).strip():
|
||||
continue
|
||||
data = json.loads(event.data)
|
||||
if data.get("mediaId") != media_id:
|
||||
continue
|
||||
collected.append(data)
|
||||
first_event.set()
|
||||
if len(collected) >= 5:
|
||||
@@ -56,13 +64,14 @@ def test_ft_p09_sse_event_delivery(
|
||||
except BaseException as e:
|
||||
thread_exc.append(e)
|
||||
finally:
|
||||
connected.set()
|
||||
first_event.set()
|
||||
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
th.start()
|
||||
time.sleep(0.5)
|
||||
r = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
assert r.status_code == 200
|
||||
connected.wait(timeout=5)
|
||||
r = http_client.post(f"/detect/{media_id}", json=body, headers=post_headers)
|
||||
assert r.status_code == 202
|
||||
first_event.wait(timeout=5)
|
||||
th.join(timeout=5)
|
||||
assert not thread_exc, thread_exc
|
||||
@@ -74,8 +83,9 @@ def test_ft_n04_duplicate_media_id_409(
|
||||
):
|
||||
media_id = "dup-test"
|
||||
body = _ai_config_image()
|
||||
headers = {"Authorization": f"Bearer {jwt_token}"}
|
||||
r1 = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
assert r1.status_code == 200
|
||||
r2 = http_client.post(f"/detect/{media_id}", json=body, headers=headers)
|
||||
headers1 = {"Authorization": f"Bearer {jwt_token}", "X-Channel-Id": str(uuid.uuid4())}
|
||||
headers2 = {"Authorization": f"Bearer {jwt_token}", "X-Channel-Id": str(uuid.uuid4())}
|
||||
r1 = http_client.post(f"/detect/{media_id}", json=body, headers=headers1)
|
||||
assert r1.status_code == 202
|
||||
r2 = http_client.post(f"/detect/{media_id}", json=body, headers=headers2)
|
||||
assert r2.status_code == 409
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import sseclient
|
||||
|
||||
_DETECT_TIMEOUT = 60
|
||||
|
||||
@@ -39,11 +43,44 @@ class TestHealthEngineStep02LazyInit:
|
||||
f"engine already initialized (aiAvailability={before['aiAvailability']}); "
|
||||
"lazy-init test must run before any test that triggers warm_engine"
|
||||
)
|
||||
|
||||
cid = str(uuid.uuid4())
|
||||
headers = {**auth_headers, "X-Channel-Id": cid}
|
||||
done = threading.Event()
|
||||
connected = threading.Event()
|
||||
|
||||
def _listen():
|
||||
try:
|
||||
with http_client.get(
|
||||
f"/detect/events/{cid}",
|
||||
stream=True,
|
||||
timeout=_DETECT_TIMEOUT + 2,
|
||||
headers=auth_headers,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
connected.set()
|
||||
for ev in sseclient.SSEClient(resp).events():
|
||||
if not ev.data or not str(ev.data).strip():
|
||||
continue
|
||||
data = json.loads(ev.data)
|
||||
if data.get("mediaStatus") in ("AIProcessed", "Error"):
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
connected.set()
|
||||
done.set()
|
||||
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
th.start()
|
||||
connected.wait(timeout=5)
|
||||
|
||||
files = {"file": ("lazy.jpg", image_small, "image/jpeg")}
|
||||
r = http_client.post("/detect/image", files=files, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
r = http_client.post("/detect/image", files=files, headers=headers, timeout=_DETECT_TIMEOUT)
|
||||
assert r.status_code == 202
|
||||
done.wait(timeout=_DETECT_TIMEOUT)
|
||||
th.join(timeout=2)
|
||||
|
||||
after = _get_health(http_client)
|
||||
_assert_active_ai(after)
|
||||
|
||||
@@ -61,13 +98,49 @@ class TestHealthEngineStep03Warmed:
|
||||
assert data.get("errorMessage") is None
|
||||
|
||||
def test_ft_p_15_onnx_cpu_detect(self, http_client, image_small, auth_headers):
|
||||
cid = str(uuid.uuid4())
|
||||
headers = {**auth_headers, "X-Channel-Id": cid}
|
||||
all_detections = []
|
||||
done = threading.Event()
|
||||
connected = threading.Event()
|
||||
|
||||
def _listen():
|
||||
try:
|
||||
with http_client.get(
|
||||
f"/detect/events/{cid}",
|
||||
stream=True,
|
||||
timeout=_DETECT_TIMEOUT + 2,
|
||||
headers=auth_headers,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
connected.set()
|
||||
for ev in sseclient.SSEClient(resp).events():
|
||||
if not ev.data or not str(ev.data).strip():
|
||||
continue
|
||||
data = json.loads(ev.data)
|
||||
if data.get("mediaStatus") == "AIProcessing":
|
||||
all_detections.extend(data.get("annotations", []))
|
||||
if data.get("mediaStatus") in ("AIProcessed", "Error"):
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
connected.set()
|
||||
done.set()
|
||||
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
th.start()
|
||||
connected.wait(timeout=5)
|
||||
|
||||
files = {"file": ("onnx.jpg", image_small, "image/jpeg")}
|
||||
r = http_client.post("/detect/image", files=files, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
r = http_client.post("/detect/image", files=files, headers=headers, timeout=_DETECT_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
if body:
|
||||
d = body[0]
|
||||
assert r.status_code == 202
|
||||
done.wait(timeout=_DETECT_TIMEOUT)
|
||||
th.join(timeout=2)
|
||||
|
||||
if all_detections:
|
||||
d = all_detections[0]
|
||||
for k in (
|
||||
"centerX",
|
||||
"centerY",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
@@ -41,6 +43,8 @@ def test_ft_n_03_loader_error_mode_detect_does_not_500(
|
||||
f"{mock_loader_url}/mock/config", json={"mode": "error"}, timeout=10
|
||||
)
|
||||
cfg.raise_for_status()
|
||||
channel_id = str(uuid.uuid4())
|
||||
headers = {**auth_headers, "X-Channel-Id": channel_id}
|
||||
files = {"file": ("small.jpg", image_small, "image/jpeg")}
|
||||
r = http_client.post("/detect/image", files=files, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
r = http_client.post("/detect/image", files=files, headers=headers, timeout=_DETECT_TIMEOUT)
|
||||
assert r.status_code != 500
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -19,20 +18,13 @@ def _percentile_ms(sorted_ms, p):
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_nft_perf_01_single_image_latency_p95(
|
||||
warm_engine, http_client, image_small, auth_headers
|
||||
warm_engine, image_detect, image_small
|
||||
):
|
||||
times_ms = []
|
||||
for _ in range(10):
|
||||
t0 = time.perf_counter()
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
timeout=8,
|
||||
)
|
||||
elapsed_ms = (time.perf_counter() - t0) * 1000.0
|
||||
assert r.status_code == 200
|
||||
_, elapsed_ms = image_detect(image_small, "img.jpg", timeout=8)
|
||||
times_ms.append(elapsed_ms)
|
||||
|
||||
sorted_ms = sorted(times_ms)
|
||||
p50 = _percentile_ms(sorted_ms, 50)
|
||||
p95 = _percentile_ms(sorted_ms, 95)
|
||||
@@ -47,34 +39,16 @@ def test_nft_perf_01_single_image_latency_p95(
|
||||
|
||||
@pytest.mark.timeout(60)
|
||||
def test_nft_perf_03_tiling_overhead_large_image(
|
||||
warm_engine, http_client, image_small, image_large, auth_headers
|
||||
warm_engine, image_detect, image_small, image_large
|
||||
):
|
||||
t_small = time.perf_counter()
|
||||
r_small = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("small.jpg", image_small, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
timeout=8,
|
||||
)
|
||||
small_ms = (time.perf_counter() - t_small) * 1000.0
|
||||
assert r_small.status_code == 200
|
||||
config = json.dumps(
|
||||
{"altitude": 400, "focal_length": 24, "sensor_width": 23.5}
|
||||
)
|
||||
t_large = time.perf_counter()
|
||||
r_large = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("large.jpg", image_large, "image/jpeg")},
|
||||
data={"config": config},
|
||||
headers=auth_headers,
|
||||
_, small_ms = image_detect(image_small, "small.jpg", timeout=8)
|
||||
_, large_ms = image_detect(
|
||||
image_large, "large.jpg",
|
||||
config=json.dumps({"altitude": 400, "focal_length": 24, "sensor_width": 23.5}),
|
||||
timeout=20,
|
||||
)
|
||||
large_ms = (time.perf_counter() - t_large) * 1000.0
|
||||
assert r_large.status_code == 200
|
||||
assert large_ms < 30_000.0
|
||||
print(
|
||||
f"nft_perf_03_csv,baseline_small_ms,{small_ms:.2f},large_ms,{large_ms:.2f}"
|
||||
)
|
||||
assert large_ms > small_ms - 500.0
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,13 @@ _DETECT_TIMEOUT = 60
|
||||
|
||||
|
||||
def test_nft_res_01_loader_outage_after_init(
|
||||
warm_engine, http_client, mock_loader_url, image_small, auth_headers
|
||||
warm_engine, image_detect, mock_loader_url, image_small, http_client
|
||||
):
|
||||
requests.post(
|
||||
f"{mock_loader_url}/mock/config", json={"mode": "error"}, timeout=10
|
||||
).raise_for_status()
|
||||
files = {"file": ("r1.jpg", image_small, "image/jpeg")}
|
||||
r = http_client.post("/detect/image", files=files, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
assert r.status_code == 200
|
||||
assert isinstance(r.json(), list)
|
||||
detections, _ = image_detect(image_small, "r1.jpg", timeout=_DETECT_TIMEOUT)
|
||||
assert isinstance(detections, list)
|
||||
h = http_client.get("/health")
|
||||
assert h.status_code == 200
|
||||
hd = h.json()
|
||||
@@ -22,15 +20,13 @@ def test_nft_res_01_loader_outage_after_init(
|
||||
|
||||
|
||||
def test_nft_res_03_transient_loader_first_fail(
|
||||
mock_loader_url, http_client, image_small, auth_headers
|
||||
mock_loader_url, image_detect, image_small
|
||||
):
|
||||
requests.post(
|
||||
f"{mock_loader_url}/mock/config", json={"mode": "first_fail"}, timeout=10
|
||||
).raise_for_status()
|
||||
files = {"file": ("r3a.jpg", image_small, "image/jpeg")}
|
||||
r1 = http_client.post("/detect/image", files=files, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
files2 = {"file": ("r3b.jpg", image_small, "image/jpeg")}
|
||||
r2 = http_client.post("/detect/image", files=files2, headers=auth_headers, timeout=_DETECT_TIMEOUT)
|
||||
assert r2.status_code == 200
|
||||
if r1.status_code != 200:
|
||||
assert r1.status_code != 500
|
||||
try:
|
||||
image_detect(image_small, "r3a.jpg", timeout=_DETECT_TIMEOUT)
|
||||
except AssertionError:
|
||||
pass
|
||||
image_detect(image_small, "r3b.jpg", timeout=_DETECT_TIMEOUT)
|
||||
|
||||
@@ -8,28 +8,16 @@ import pytest
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.timeout(120)
|
||||
def test_nft_res_lim_03_max_detections_per_frame(
|
||||
warm_engine, http_client, image_dense, auth_headers
|
||||
warm_engine, image_detect, image_dense
|
||||
):
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_dense, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
timeout=120,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
assert len(body) <= 300
|
||||
detections, _ = image_detect(image_dense, "img.jpg", timeout=120)
|
||||
assert isinstance(detections, list)
|
||||
assert len(detections) <= 300
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_nft_res_lim_04_log_file_rotation(warm_engine, http_client, image_small, auth_headers):
|
||||
http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
timeout=60,
|
||||
)
|
||||
def test_nft_res_lim_04_log_file_rotation(warm_engine, image_detect, image_small):
|
||||
image_detect(image_small, "img.jpg", timeout=60)
|
||||
candidates = [
|
||||
Path(__file__).resolve().parent.parent / "logs",
|
||||
Path("/app/Logs"),
|
||||
|
||||
@@ -81,16 +81,10 @@ def _weather_label_ok(label, base_names):
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm_engine, auth_headers):
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
for d in body:
|
||||
def test_ft_p_03_detection_response_structure_ac1(image_detect, image_small, warm_engine):
|
||||
detections, _ = image_detect(image_small, "img.jpg")
|
||||
assert isinstance(detections, list)
|
||||
for d in detections:
|
||||
assert isinstance(d["centerX"], (int, float))
|
||||
assert isinstance(d["centerY"], (int, float))
|
||||
assert isinstance(d["width"], (int, float))
|
||||
@@ -106,44 +100,24 @@ def test_ft_p_03_detection_response_structure_ac1(http_client, image_small, warm
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_05_confidence_filtering_ac2(http_client, image_small, warm_engine, auth_headers):
|
||||
cfg_hi = json.dumps({"probability_threshold": 0.8})
|
||||
r_hi = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
data={"config": cfg_hi},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert r_hi.status_code == 200
|
||||
hi = r_hi.json()
|
||||
def test_ft_p_05_confidence_filtering_ac2(image_detect, image_small, warm_engine):
|
||||
hi, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.8}))
|
||||
assert isinstance(hi, list)
|
||||
for d in hi:
|
||||
assert float(d["confidence"]) + _EPS >= 0.8
|
||||
cfg_lo = json.dumps({"probability_threshold": 0.1})
|
||||
r_lo = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
data={"config": cfg_lo},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert r_lo.status_code == 200
|
||||
lo = r_lo.json()
|
||||
|
||||
lo, _ = image_detect(image_small, "img.jpg", config=json.dumps({"probability_threshold": 0.1}))
|
||||
assert isinstance(lo, list)
|
||||
assert len(lo) >= len(hi)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine, auth_headers):
|
||||
cfg_loose = json.dumps({"tracking_intersection_threshold": 0.6})
|
||||
r1 = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_dense, "image/jpeg")},
|
||||
data={"config": cfg_loose},
|
||||
headers=auth_headers,
|
||||
def test_ft_p_06_overlap_deduplication_ac3(image_detect, image_dense, warm_engine):
|
||||
dets, _ = image_detect(
|
||||
image_dense, "img.jpg",
|
||||
config=json.dumps({"tracking_intersection_threshold": 0.6}),
|
||||
timeout=_DETECT_SLOW_TIMEOUT,
|
||||
)
|
||||
assert r1.status_code == 200
|
||||
dets = r1.json()
|
||||
assert isinstance(dets, list)
|
||||
by_label = {}
|
||||
for d in dets:
|
||||
@@ -153,22 +127,18 @@ def test_ft_p_06_overlap_deduplication_ac3(http_client, image_dense, warm_engine
|
||||
for j in range(i + 1, len(group)):
|
||||
ratio = _overlap_to_min_area_ratio(group[i], group[j])
|
||||
assert ratio <= 0.6 + _EPS, (label, ratio)
|
||||
cfg_strict = json.dumps({"tracking_intersection_threshold": 0.01})
|
||||
r2 = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_dense, "image/jpeg")},
|
||||
data={"config": cfg_strict},
|
||||
headers=auth_headers,
|
||||
|
||||
strict, _ = image_detect(
|
||||
image_dense, "img.jpg",
|
||||
config=json.dumps({"tracking_intersection_threshold": 0.01}),
|
||||
timeout=_DETECT_SLOW_TIMEOUT,
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
strict = r2.json()
|
||||
assert isinstance(strict, list)
|
||||
assert len(strict) <= len(dets)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engine, auth_headers):
|
||||
def test_ft_p_07_physical_size_filtering_ac4(image_detect, image_small, warm_engine):
|
||||
by_id, _ = _load_classes_media()
|
||||
wh = _image_width_height(image_small)
|
||||
assert wh is not None
|
||||
@@ -184,15 +154,7 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
|
||||
"sensor_width": sensor_width,
|
||||
}
|
||||
)
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_small, "image/jpeg")},
|
||||
data={"config": cfg},
|
||||
headers=auth_headers,
|
||||
timeout=_DETECT_SLOW_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
body, _ = image_detect(image_small, "img.jpg", config=cfg, timeout=_DETECT_SLOW_TIMEOUT)
|
||||
assert isinstance(body, list)
|
||||
for d in body:
|
||||
base_id = d["classNum"] % _WEATHER_CLASS_STRIDE
|
||||
@@ -203,17 +165,10 @@ def test_ft_p_07_physical_size_filtering_ac4(http_client, image_small, warm_engi
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_13_weather_mode_class_variants_ac5(
|
||||
http_client, image_different_types, warm_engine, auth_headers
|
||||
image_detect, image_different_types, warm_engine
|
||||
):
|
||||
_, base_names = _load_classes_media()
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_different_types, "image/jpeg")},
|
||||
headers=auth_headers,
|
||||
timeout=_DETECT_SLOW_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
body, _ = image_detect(image_different_types, "img.jpg", timeout=_DETECT_SLOW_TIMEOUT)
|
||||
assert isinstance(body, list)
|
||||
for d in body:
|
||||
label = d["label"]
|
||||
|
||||
@@ -10,6 +10,7 @@ Run with: pytest e2e/tests/test_streaming_video_upload.py -s -v
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -37,21 +38,23 @@ def _chunked_reader(path: str, chunk_size: int = 64 * 1024):
|
||||
|
||||
|
||||
def _start_sse_listener(
|
||||
http_client, media_id: str, auth_headers: dict
|
||||
http_client, channel_id: str, auth_headers: dict
|
||||
) -> tuple[list[dict], list[BaseException], threading.Event]:
|
||||
events: list[dict] = []
|
||||
errors: list[BaseException] = []
|
||||
first_event = threading.Event()
|
||||
connected = threading.Event()
|
||||
|
||||
def _listen():
|
||||
try:
|
||||
with http_client.get(
|
||||
f"/detect/{media_id}",
|
||||
f"/detect/events/{channel_id}",
|
||||
stream=True,
|
||||
timeout=_TIMEOUT + 2,
|
||||
headers=auth_headers,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
connected.set()
|
||||
for event in sseclient.SSEClient(resp).events():
|
||||
if not event.data or not str(event.data).strip():
|
||||
continue
|
||||
@@ -62,9 +65,12 @@ def _start_sse_listener(
|
||||
except BaseException as exc:
|
||||
errors.append(exc)
|
||||
finally:
|
||||
connected.set()
|
||||
first_event.set()
|
||||
|
||||
threading.Thread(target=_listen, daemon=True).start()
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
th.start()
|
||||
connected.wait(timeout=3)
|
||||
return events, errors, first_event
|
||||
|
||||
|
||||
@@ -74,6 +80,8 @@ def test_streaming_video_detections_appear_during_upload(
|
||||
):
|
||||
# Arrange
|
||||
video_path = _fixture_path("video_test01.mp4")
|
||||
channel_id = str(uuid.uuid4())
|
||||
events, errors, first_event = _start_sse_listener(http_client, channel_id, auth_headers)
|
||||
|
||||
# Act
|
||||
r = http_client.post(
|
||||
@@ -81,14 +89,13 @@ def test_streaming_video_detections_appear_during_upload(
|
||||
data=_chunked_reader(video_path),
|
||||
headers={
|
||||
**auth_headers,
|
||||
"X-Channel-Id": channel_id,
|
||||
"X-Filename": "video_test01.mp4",
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=8,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
media_id = r.json()["mediaId"]
|
||||
events, errors, first_event = _start_sse_listener(http_client, media_id, auth_headers)
|
||||
assert r.status_code == 202
|
||||
first_event.wait(timeout=_TIMEOUT)
|
||||
|
||||
# Assert
|
||||
@@ -103,6 +110,8 @@ def test_streaming_video_detections_appear_during_upload(
|
||||
def test_non_faststart_video_still_works(warm_engine, http_client, auth_headers):
|
||||
# Arrange
|
||||
video_path = _fixture_path("video_test01.mp4")
|
||||
channel_id = str(uuid.uuid4())
|
||||
events, errors, first_event = _start_sse_listener(http_client, channel_id, auth_headers)
|
||||
|
||||
# Act
|
||||
r = http_client.post(
|
||||
@@ -110,14 +119,13 @@ def test_non_faststart_video_still_works(warm_engine, http_client, auth_headers)
|
||||
data=_chunked_reader(video_path),
|
||||
headers={
|
||||
**auth_headers,
|
||||
"X-Channel-Id": channel_id,
|
||||
"X-Filename": "video_test01_plain.mp4",
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=8,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
media_id = r.json()["mediaId"]
|
||||
events, errors, first_event = _start_sse_listener(http_client, media_id, auth_headers)
|
||||
assert r.status_code == 202
|
||||
first_event.wait(timeout=_TIMEOUT)
|
||||
|
||||
# Assert
|
||||
|
||||
@@ -28,32 +28,22 @@ def _assert_no_same_label_near_duplicate_centers(detections):
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_04_gsd_based_tiling_ac1(http_client, image_large, warm_engine, auth_headers):
|
||||
config = json.dumps(_GSD)
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_large, "image/jpeg")},
|
||||
data={"config": config},
|
||||
headers=auth_headers,
|
||||
def test_ft_p_04_gsd_based_tiling_ac1(image_detect, image_large, warm_engine):
|
||||
body, _ = image_detect(
|
||||
image_large, "img.jpg",
|
||||
config=json.dumps(_GSD),
|
||||
timeout=_TILING_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
_assert_coords_normalized(body)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ft_p_16_tile_boundary_deduplication_ac2(http_client, image_large, warm_engine, auth_headers):
|
||||
config = json.dumps({**_GSD, "big_image_tile_overlap_percent": 20})
|
||||
r = http_client.post(
|
||||
"/detect/image",
|
||||
files={"file": ("img.jpg", image_large, "image/jpeg")},
|
||||
data={"config": config},
|
||||
headers=auth_headers,
|
||||
def test_ft_p_16_tile_boundary_deduplication_ac2(image_detect, image_large, warm_engine):
|
||||
body, _ = image_detect(
|
||||
image_large, "img.jpg",
|
||||
config=json.dumps({**_GSD, "big_image_tile_overlap_percent": 20}),
|
||||
timeout=_TILING_TIMEOUT,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert isinstance(body, list)
|
||||
_assert_no_same_label_near_duplicate_centers(body)
|
||||
|
||||
+24
-14
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -24,29 +25,22 @@ def video_events(warm_engine, http_client, auth_headers):
|
||||
if not Path(_VIDEO).is_file():
|
||||
pytest.skip(f"missing fixture {_VIDEO}")
|
||||
|
||||
r = http_client.post(
|
||||
"/detect/video",
|
||||
data=_chunked_reader(_VIDEO),
|
||||
headers={
|
||||
**auth_headers,
|
||||
"X-Filename": "video_test01.mp4",
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
media_id = r.json()["mediaId"]
|
||||
|
||||
channel_id = str(uuid.uuid4())
|
||||
collected: list[tuple[float, dict]] = []
|
||||
thread_exc: list[BaseException] = []
|
||||
done = threading.Event()
|
||||
connected = threading.Event()
|
||||
|
||||
def _listen():
|
||||
try:
|
||||
with http_client.get(
|
||||
f"/detect/{media_id}", stream=True, timeout=35, headers=auth_headers
|
||||
f"/detect/events/{channel_id}",
|
||||
stream=True,
|
||||
timeout=60,
|
||||
headers=auth_headers,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
connected.set()
|
||||
sse = sseclient.SSEClient(resp)
|
||||
for event in sse.events():
|
||||
if not event.data or not str(event.data).strip():
|
||||
@@ -61,10 +55,26 @@ def video_events(warm_engine, http_client, auth_headers):
|
||||
except BaseException as e:
|
||||
thread_exc.append(e)
|
||||
finally:
|
||||
connected.set()
|
||||
done.set()
|
||||
|
||||
th = threading.Thread(target=_listen, daemon=True)
|
||||
th.start()
|
||||
connected.wait(timeout=5)
|
||||
|
||||
r = http_client.post(
|
||||
"/detect/video",
|
||||
data=_chunked_reader(_VIDEO),
|
||||
headers={
|
||||
**auth_headers,
|
||||
"X-Channel-Id": channel_id,
|
||||
"X-Filename": "video_test01.mp4",
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
assert r.status_code == 202
|
||||
|
||||
assert done.wait(timeout=30)
|
||||
th.join(timeout=5)
|
||||
assert not thread_exc, thread_exc
|
||||
|
||||
Reference in New Issue
Block a user