Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .zektopic/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,15 @@ Based on the full-codebase testing evaluation, here are specific features and op
- **Mock Enhancements**: If `test_runner.py` is maintained, the `MockNumpy`, `MockPydanticValidationError`, and `MockBaseModel` classes need significant upgrades to accurately replicate complex C-extension behaviors (like array shape comparisons and multi-dimensional indexing) and Pydantic v2 nested schema validation.

#### B. Video Processing & Path Validation Fixes
- **Video Detection Logic**: The tests in `test_video.py` reveal failures in `reduce_detections` and `get_cluster_candidates`. The logic needs to be reviewed to handle different size overlapping objects and vertical stacking without incorrectly reducing detections. Ensure robust type handling (e.g., `isinstance(index, (int, np.integer))`) when iterating through `cv2.dnn.NMSBoxes` results.
- **Video Detection Logic**: The tests in `test_video.py` reveal failures in `reduce_detections` and `get_cluster_candidates`. The logic needs to be reviewed to handle different size overlapping objects and vertical stacking without incorrectly reducing detections. (Fixed: `cv2.dnn.NMSBoxes` results iteration type handling).
- **Path Validation**: The `sanitize_path_component` function in `frigate/util/path.py` (which relies on `pathvalidate`) fails when testing for relative markers (e.g., `.` or `..`). The custom sanitization logic should be reviewed to ensure it correctly identifies and rejects these traversal markers.

#### C. Frontend Modernization
- **Punycode Replacement**: Update the frontend dependencies (`tr46`, `whatwg-url`) to replace the deprecated `punycode` module. This will silence the `DEP0040` warnings during test runs and improve future compatibility.

#### D. Rust Optimization
- **Code Cleanup**: Address the unused variable, unused function, and unnecessary `mut` binding warnings highlighted during the Rust `cargo test` runs.


## Ad-Hoc Testing Limitations
The tests for `TestObjectBoundingBoxes` and `TestRegion` in `test_video.py` currently fail natively via `test_runner.py` due to limited support for numpy array comparisons on Mock objects in the ad-hoc test runner, which is currently unavoidable outside Docker unless tests are heavily modified or a full integration environment is spun up. Same goes for missing modules like `peewee` or `cv2`.
2 changes: 1 addition & 1 deletion Jules/improvements.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Based on the test runs and codebase review, here are several suggested improveme

## Testing Improvements
- Test suite fails to run because dependencies mock in `test_runner.py` is incomplete. Should mock additional modules such as `peewee`, `playhouse`, `unidecode`, `filelock`, `fastapi`, `httpx`, `peewee_migrate`, `pytz`, `scipy`, `sherpa_onnx`, `zeep`, `norfair.camera_motion`, `onvif`, and some missing `pydantic` fields to fix 65 failing tests.
- Backend tests also face TypeErrors during image processing. For example, `test_crop_yuv` throws `< not supported between instances of int and MagicMock` because it's using mocked cv2 methods where mock isn't sufficient.
- Fixed python unit test mock errors in cv2.dnn.NMSBoxes where index was not checked if it is a list or an integer. The tests for TestObjectBoundingBoxes and TestRegion in test_video.py still fail due to limited support for numpy array comparisons on Mock objects in the ad-hoc test runner, which is currently unavoidable outside Docker unless tests are heavily modified or a full integration environment is spun up.
- Python 3.12 compatibility issues inside `test_runner.py` mocks cause Pydantic to throw `TypeError: FrigateConfig() takes no arguments`.
- Fix python unit test mock errors. Improve `test_runner.py` to fully mock `peewee`, `pydantic` (with `AfterValidator`, `ValidationInfo`), `unidecode`, and `filelock`.
- Ensure frontend vitest tests use explicitly passed timezones when evaluating formatting, preventing timezone-dependent flakiness across platforms.
Expand Down
5 changes: 4 additions & 1 deletion frigate/api/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@ def register_notifications(request: Request, body: dict = None):

if not username or username == "anonymous":
return JSONResponse(
content={"success": False, "message": "Cannot register notifications for an anonymous user."},
content={
"success": False,
"message": "Cannot register notifications for an anonymous user.",
},
status_code=400,
)

Expand Down
4 changes: 3 additions & 1 deletion frigate/db/sqlitevecq.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ def _connect(self, *args: Any, **kwargs: Any) -> sqlite3.Connection:
conn: sqlite3.Connection = super()._connect(*args, **kwargs) # type: ignore[misc]
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA synchronous=NORMAL;")
conn.execute("PRAGMA busy_timeout=30000;") # 30-sec busy timeout prevents "database is locked" errors
conn.execute(
"PRAGMA busy_timeout=30000;"
) # 30-sec busy timeout prevents "database is locked" errors
conn.execute("PRAGMA temp_store=MEMORY;")
conn.execute("PRAGMA mmap_size=268435456;") # 256 MB — reduces read() syscalls
conn.execute("PRAGMA wal_autocheckpoint=1000;")
Expand Down
8 changes: 5 additions & 3 deletions frigate/test/test_frame_shm_rust.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import unittest

import numpy as np

from frigate.util.frame_rs import (
frame_rs_available,
intersection_over_union_rust,
track_distance_rust,
preprocess_detect_input_rust,
track_distance_rust,
)


class TestFrameShmRust(unittest.TestCase):
def test_frame_rs_available(self):
"""Ensure Rust frame engine library loads correctly."""
self.assertTrue(frame_rs_available(), "libfrigate_frame_rs.so should be available")
self.assertTrue(
frame_rs_available(), "libfrigate_frame_rs.so should be available"
)

def test_iou_rust(self):
"""Test bounding box IoU calculation in Rust."""
Expand Down Expand Up @@ -59,6 +62,5 @@ def test_preprocess_detect_input_rust(self):
self.assertTrue(np.all(out_np >= 0.0) and np.all(out_np <= 1.0))



if __name__ == "__main__":
unittest.main()
54 changes: 39 additions & 15 deletions frigate/test/test_fuzzing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@
import math
import random
import unittest

import numpy as np

from frigate.detectors.rust_yolo import (
yolo26_post_process,
yolo_available,
)
from frigate.util.frame_rs import (
fast_shm_copy_rust,
frame_rs_available,
point_in_polygon_rust,
polygon_box_overlap_rust,
intersection_over_union_rust,
track_distance_rust,
fast_shm_copy_rust,
)
from frigate.detectors.rust_yolo import (
yolo_available,
yolo26_post_process,
)


Expand All @@ -31,7 +31,24 @@ def test_fuzz_fast_shm_copy_random_lengths(self):
self.skipTest("Rust frame engine not available")

# Test various aligned and unaligned lengths
test_lengths = [0, 1, 7, 15, 16, 31, 32, 33, 63, 64, 65, 127, 128, 513, 1024, 65537]
test_lengths = [
0,
1,
7,
15,
16,
31,
32,
33,
63,
64,
65,
127,
128,
513,
1024,
65537,
]
for length in test_lengths:
if length == 0:
continue
Expand All @@ -58,14 +75,16 @@ def test_fuzz_track_distance_nan_inf_degenerate(self):
[float("inf"), 10.0, 100.0, 100.0],
[10.0, float("-inf"), 100.0, 100.0],
[100.0, 100.0, 10.0, 10.0], # Inverted box (x2 < x1, y2 < y1)
[50.0, 50.0, 50.0, 50.0], # Zero-width / zero-height box
[-1e9, -1e9, 1e9, 1e9], # Extreme coordinates
[50.0, 50.0, 50.0, 50.0], # Zero-width / zero-height box
[-1e9, -1e9, 1e9, 1e9], # Extreme coordinates
]

for bad_box in extreme_cases:
dist = track_distance_rust(bad_box, valid_box)
# Must return finite float or +inf without panic or segfault
self.assertTrue(math.isnan(dist) or math.isinf(dist) or isinstance(dist, float))
self.assertTrue(
math.isnan(dist) or math.isinf(dist) or isinstance(dist, float)
)

def test_fuzz_polygon_geometry_extreme_points(self):
"""Fuzz point-in-polygon and polygon-box overlap with complex / self-intersecting polygons."""
Expand All @@ -78,7 +97,9 @@ def test_fuzz_polygon_geometry_extreme_points(self):

# 2. Single point / 2-point line segment
self.assertFalse(point_in_polygon_rust(50.0, 50.0, [(10.0, 10.0)]))
self.assertFalse(point_in_polygon_rust(50.0, 50.0, [(10.0, 10.0), (20.0, 20.0)]))
self.assertFalse(
point_in_polygon_rust(50.0, 50.0, [(10.0, 10.0), (20.0, 20.0)])
)

# 3. Huge self-intersecting bowtie polygon
bowtie = [(0.0, 0.0), (100.0, 100.0), (0.0, 100.0), (100.0, 0.0)]
Expand All @@ -87,8 +108,7 @@ def test_fuzz_polygon_geometry_extreme_points(self):

# 4. Fuzz with 1000 random points against a complex 20-vertex polygon
polygon = [
(random.uniform(0, 1000), random.uniform(0, 1000))
for _ in range(20)
(random.uniform(0, 1000), random.uniform(0, 1000)) for _ in range(20)
]
for _ in range(100):
px = random.uniform(-100, 1100)
Expand All @@ -103,15 +123,19 @@ def test_fuzz_yolo26_post_process_corrupted_tensors(self):

# Random tensor of shape (84, 8400)
raw_noise = np.random.uniform(-10.0, 10.0, (84, 8400)).astype(np.float32)
dets = yolo26_post_process(raw_noise, model_size=640, frame_w=1.0, frame_h=1.0, score_thresh=0.5)
dets = yolo26_post_process(
raw_noise, model_size=640, frame_w=1.0, frame_h=1.0, score_thresh=0.5
)
self.assertEqual(dets.shape, (20, 6))

# Tensor containing NaNs and Infs
raw_corrupt = np.zeros((84, 100), dtype=np.float32)
raw_corrupt[0, :] = np.nan
raw_corrupt[1, :] = np.inf
raw_corrupt[4, :] = 0.9 # high class score
dets_corrupt = yolo26_post_process(raw_corrupt, model_size=640, frame_w=1.0, frame_h=1.0)
dets_corrupt = yolo26_post_process(
raw_corrupt, model_size=640, frame_w=1.0, frame_h=1.0
)
self.assertEqual(dets_corrupt.shape, (20, 6))


Expand Down
2 changes: 1 addition & 1 deletion frigate/test/test_notification_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
sys.modules.setdefault("zmq", MagicMock())
sys.modules.setdefault("frigate.version", MagicMock())

from frigate.api.notification import register_notifications
from frigate.api.notification import register_notifications # noqa: E402


class TestNotificationApi(unittest.TestCase):
Expand Down
8 changes: 6 additions & 2 deletions frigate/test/test_smoke_physical.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import os
import unittest

import numpy as np


Expand All @@ -22,7 +23,7 @@ def test_physical_vulkan_gpu_compute(self):

# Test Net initialization with Vulkan options
net = ncnn.Net()
net.opt.use_vulkan_compute = (gpu_count > 0)
net.opt.use_vulkan_compute = gpu_count > 0
net.opt.use_fp16_arithmetic = True
net.opt.use_fp16_packed = True
net.opt.use_fp16_storage = True
Expand All @@ -31,7 +32,9 @@ def test_physical_vulkan_gpu_compute(self):
bin_path = "/config/model_cache/yolo26n.bin"

if not os.path.exists(param_path) or not os.path.exists(bin_path):
self.skipTest(f"Model files {param_path} / {bin_path} not found in test environment")
self.skipTest(
f"Model files {param_path} / {bin_path} not found in test environment"
)

net.load_param(param_path)
net.load_model(bin_path)
Expand All @@ -54,6 +57,7 @@ def test_isolated_api_smoke_harness(self):
"""Smoke test API route handlers in isolation without binding production port 5000."""
from fastapi import FastAPI
from fastapi.testclient import TestClient

from frigate.version import VERSION

test_app = FastAPI()
Expand Down
1 change: 1 addition & 0 deletions frigate/test/test_sqlite_wal_queue.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import tempfile
import unittest

from frigate.db.sqlitevecq import SqliteVecQueueDatabase


Expand Down
27 changes: 15 additions & 12 deletions frigate/test/test_stress_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@
import threading
import time
import unittest
import numpy as np

from frigate.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.util.frame_rs import (
frame_rs_available,
batch_track_distance_matrix_rust,
fast_shm_copy_rust,
frame_rs_available,
)


Expand All @@ -30,6 +28,7 @@ def tearDown(self):

def _init_schema(self):
import sqlite3

conn = sqlite3.connect(self.db_path, timeout=30.0)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA synchronous=NORMAL;")
Expand All @@ -51,6 +50,7 @@ def _init_schema(self):
def test_sqlite_concurrent_writers_stress(self):
"""Stress test SQLite database with 30 concurrent threads performing rapid inserts."""
import sqlite3

num_threads = 30
inserts_per_thread = 50
errors = []
Expand All @@ -73,8 +73,7 @@ def worker(thread_idx: int):
errors.append((thread_idx, e))

threads = [
threading.Thread(target=worker, args=(t,))
for t in range(num_threads)
threading.Thread(target=worker, args=(t,)) for t in range(num_threads)
]
for t in threads:
t.start()
Expand All @@ -98,10 +97,7 @@ def test_tracker_distance_matrix_high_density_stress(self):
n_dets = 100
n_ests = 100

dets = [
(i * 5.0, i * 5.0, (i + 2) * 5.0, (i + 2) * 5.0)
for i in range(n_dets)
]
dets = [(i * 5.0, i * 5.0, (i + 2) * 5.0, (i + 2) * 5.0) for i in range(n_dets)]
ests = [
(j * 5.0 + 1.0, j * 5.0 + 1.0, (j + 2) * 5.0 + 1.0, (j + 2) * 5.0 + 1.0)
for j in range(n_ests)
Expand All @@ -115,14 +111,17 @@ def test_tracker_distance_matrix_high_density_stress(self):
elapsed = time.perf_counter() - t0

# 50 runs of 10,000 comparisons (500,000 total) should execute in < 150ms in Rust
self.assertLess(elapsed, 0.5, f"Vectorized tracker distance exceeded budget: {elapsed:.3f}s")
self.assertLess(
elapsed, 0.5, f"Vectorized tracker distance exceeded budget: {elapsed:.3f}s"
)

def test_sustained_zero_copy_simd_throughput(self):
"""Benchmark and stress test fast_shm_copy with 1,000 1080p frame copies."""
if not frame_rs_available():
self.skipTest("Rust frame engine not available")

import ctypes

# 1080p RGB frame size = 1920 * 1080 * 3 = 6,220,800 bytes (~6.2 MB)
frame_size = 1920 * 1080 * 3
src_data = bytearray(frame_size)
Expand All @@ -137,10 +136,14 @@ def test_sustained_zero_copy_simd_throughput(self):
fast_shm_copy_rust(dst_buf, src_buf, frame_size)
elapsed = time.perf_counter() - t0

total_gb = (frame_size * iterations) / (1024 ** 3)
total_gb = (frame_size * iterations) / (1024**3)
throughput_gbps = total_gb / elapsed
# Assert throughput is high-performance (> 5 GB/s)
self.assertGreater(throughput_gbps, 1.0, f"SIMD throughput too slow: {throughput_gbps:.2f} GB/s")
self.assertGreater(
throughput_gbps,
1.0,
f"SIMD throughput too slow: {throughput_gbps:.2f} GB/s",
)


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion frigate/test/test_tracker_rust.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import unittest

from frigate.util.frame_rs import (
batch_track_distance_matrix_rust,
frame_rs_available,
track_distance_rust,
batch_track_distance_matrix_rust,
)


Expand Down
4 changes: 1 addition & 3 deletions frigate/track/norfair_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,7 @@ def deregister(self, id: str, track_id: str) -> None:
):
tracker = self.get_tracker(obj["label"])
tracker.tracked_objects = [
o
for o in tracker.tracked_objects
if str(o.global_id) != track_id
o for o in tracker.tracked_objects if str(o.global_id) != track_id
]

del self.track_id_map[track_id]
Expand Down
5 changes: 4 additions & 1 deletion frigate/util/frame_rs.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ def point_in_polygon_rust(px: float, py: float, pts: list[tuple[float, float]])
return bool(lib.point_in_polygon(px, py, arr, len(pts)))


def polygon_box_overlap_rust(pts: list[tuple[float, float]], box: tuple[float, float, float, float]) -> bool:
def polygon_box_overlap_rust(
pts: list[tuple[float, float]], box: tuple[float, float, float, float]
) -> bool:
"""Check if bounding box [x1, y1, x2, y2] overlaps with polygon in Rust."""
if len(pts) < 3:
return False
Expand All @@ -157,6 +159,7 @@ def polygon_box_overlap_rust(pts: list[tuple[float, float]], box: tuple[float, f
def batch_track_distance_matrix_rust(detections: list, estimates: list):
"""Vectorized NxM pairwise tracker distance matrix in Rust."""
import numpy as np

n_dets = len(detections)
n_ests = len(estimates)
if n_dets == 0 or n_ests == 0:
Expand Down
4 changes: 3 additions & 1 deletion frigate/util/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ def post_process_dfine(
return detections


def post_process_rfdetr(tensor_output: list[np.ndarray] | tuple[np.ndarray, ...]) -> np.ndarray:
def post_process_rfdetr(
tensor_output: list[np.ndarray] | tuple[np.ndarray, ...],
) -> np.ndarray:
boxes = tensor_output[0]
raw_scores = tensor_output[1]

Expand Down
Loading
Loading