diff --git a/.zektopic/status.md b/.zektopic/status.md index 7f5c832c136..572caf2c1f6 100644 --- a/.zektopic/status.md +++ b/.zektopic/status.md @@ -238,3 +238,43 @@ The tests for `TestObjectBoundingBoxes` and `TestRegion` in `test_video.py` curr - Fixed `test_util_path.py` missing `pathvalidate` dependency by installing it. - Identified that `test_object_detector.py` requires a better mock for `test_cfg.model` to prevent `AttributeError`. - Documented issue in `Jules/improvements.md`. + +## Test Results and Future Implementations Roadmap (Current Iteration) + +### Frontend Tests +- Executed `cd web && npm ci && npm run test -- --run src/`. +- All 137 unit tests across 13 files passed successfully. +- Deprecation warnings (`DEP0040`) for `punycode` continue to be logged. +- Executed `cd web && npm run e2e`. +- Tests run successfully. + +### Rust Backend Tests +- Executed `cargo test` against `frigate-detector-rs`, `frigate-frame-rs`, `frigate-motion-rs`, and `frigate-yolo-rs`. +- All standard and pixel pipeline tests pass completely (47 tests total). No errors or failures. + +### Python Backend Tests (Native Docker) +- Attempted to run the backend test suite via `make run_tests`. +- The build process using `docker buildx build` continues to fail entirely due to a host-level BuildKit `overlayfs` mount error (`invalid argument`). Native execution remains impossible on this environment until the storage driver is fixed or BuildKit is bypassed. + +### Python Backend Tests (Local Fallback) +- Executed `python3 test_runner.py` outside of Docker. +- The script executes the 758 tests but generates significant false negatives (89 failures, 201 errors). +- Pathvalidate dependency was missing. Installed `pathvalidate` and verified it fixed some path tests, but some failures still occur natively outside Docker. +- Video and region tests 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`. + +### Actionable Roadmap of Future Implementations and Improvements + +#### A. Backend & Testing Environment +- **Docker BuildKit Fix**: The local Docker execution (`make run_tests`) must be configured to bypass the `overlayfs` mount error (e.g., using `DOCKER_BUILDKIT=0` or a different storage driver). This is critical to run the backend tests natively and eliminate the need for the brittle `test_runner.py` mocks. +- **Dependency Management**: Missing dependencies like `pathvalidate`, `numpy`, and `opencv-python-headless` cause failures in the local fallback test runner. A dedicated `requirements-test.txt` or a `tox.ini` setup should be created to manage dependencies for local testing outside of Docker. +- **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. (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. diff --git a/Jules/improvements.md b/Jules/improvements.md index c7aa658429e..d234d8fb69f 100644 --- a/Jules/improvements.md +++ b/Jules/improvements.md @@ -266,3 +266,43 @@ Based on the full-codebase testing evaluation, here are specific features and op ## Test Object Detector Fixes - In `test_object_detector.py`, test cases expect a model attribute to be present in `test_cfg` mock but fails due to `AttributeError: Mock object has no attribute 'model'`. Improve mocking inside `frigate.test.test_object_detector` or `test_runner.py` to fix this issue. + +## Test Results and Future Implementations Roadmap (Current Iteration) + +### Frontend Tests +- Executed `cd web && npm ci && npm run test -- --run src/`. +- All 137 unit tests across 13 files passed successfully. +- Deprecation warnings (`DEP0040`) for `punycode` continue to be logged. +- Executed `cd web && npm run e2e`. +- Tests run successfully. + +### Rust Backend Tests +- Executed `cargo test` against `frigate-detector-rs`, `frigate-frame-rs`, `frigate-motion-rs`, and `frigate-yolo-rs`. +- All standard and pixel pipeline tests pass completely (47 tests total). No errors or failures. + +### Python Backend Tests (Native Docker) +- Attempted to run the backend test suite via `make run_tests`. +- The build process using `docker buildx build` continues to fail entirely due to a host-level BuildKit `overlayfs` mount error (`invalid argument`). Native execution remains impossible on this environment until the storage driver is fixed or BuildKit is bypassed. + +### Python Backend Tests (Local Fallback) +- Executed `python3 test_runner.py` outside of Docker. +- The script executes the 758 tests but generates significant false negatives (89 failures, 201 errors). +- Pathvalidate dependency was missing. Installed `pathvalidate` and verified it fixed some path tests, but some failures still occur natively outside Docker. +- Video and region tests 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`. + +### Actionable Roadmap of Future Implementations and Improvements + +#### A. Backend & Testing Environment +- **Docker BuildKit Fix**: The local Docker execution (`make run_tests`) must be configured to bypass the `overlayfs` mount error (e.g., using `DOCKER_BUILDKIT=0` or a different storage driver). This is critical to run the backend tests natively and eliminate the need for the brittle `test_runner.py` mocks. +- **Dependency Management**: Missing dependencies like `pathvalidate`, `numpy`, and `opencv-python-headless` cause failures in the local fallback test runner. A dedicated `requirements-test.txt` or a `tox.ini` setup should be created to manage dependencies for local testing outside of Docker. +- **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. (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. diff --git a/frigate/util/frame_rs.py b/frigate/util/frame_rs.py index f6859fa7ecc..19a91f4c19b 100644 --- a/frigate/util/frame_rs.py +++ b/frigate/util/frame_rs.py @@ -212,3 +212,42 @@ def fast_shm_copy_rust(dst_buf, src_buf, length: int) -> None: src_ptr = ctypes.addressof(ctypes.c_char.from_buffer(src_buf)) lib.fast_shm_copy(dst_ptr, src_ptr, ctypes.c_size_t(length)) + +def preprocess_detect_input_rust( + src_bytes: bytes, + src_w: int, + src_h: int, + dst_w: int, + dst_h: int, + channels: int = 3, +): + import ctypes + lib = _load_lib() + if lib is None: + raise RuntimeError("Rust frame engine not available") + + lib.preprocess_detect_input.argtypes = [ + ctypes.c_void_p, + ctypes.c_void_p, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.c_uint32, + ctypes.c_uint32, + ] + lib.preprocess_detect_input.restype = None + + out_buf = bytearray(dst_w * dst_h * channels * 4) # float32 = 4 bytes + src_ptr = ctypes.c_char_p(src_bytes) + dst_ptr = ctypes.addressof(ctypes.c_char.from_buffer(out_buf)) + + lib.preprocess_detect_input( + src_ptr, + dst_ptr, + src_w, + src_h, + dst_w, + dst_h, + channels, + ) + return out_buf diff --git a/migrations/002_add_clip_snapshot.py b/migrations/002_add_clip_snapshot.py index 47a46f5728f..880121d68da 100644 --- a/migrations/002_add_clip_snapshot.py +++ b/migrations/002_add_clip_snapshot.py @@ -23,18 +23,19 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', has_clip=pw.BooleanField(default=True), has_snapshot=pw.BooleanField(default=True), ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["has_clip", "has_snapshot"]) + migrator.remove_fields( + 'event', ["has_clip", "has_snapshot"]) diff --git a/migrations/004_add_bbox_region_area.py b/migrations/004_add_bbox_region_area.py index a1aa35aab20..51b4835251c 100644 --- a/migrations/004_add_bbox_region_area.py +++ b/migrations/004_add_bbox_region_area.py @@ -24,14 +24,14 @@ import peewee as pw from playhouse.sqlite_ext import JSONField -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', region=JSONField(default=[]), box=JSONField(default=[]), area=pw.IntegerField(default=0), @@ -39,4 +39,5 @@ def migrate(migrator, database, fake=False, **kwargs): def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["region", "box", "area"]) + migrator.remove_fields( + 'event', ["region", "box", "area"]) diff --git a/migrations/005_make_end_time_nullable.py b/migrations/005_make_end_time_nullable.py index d80d31d881d..03428ba3eca 100644 --- a/migrations/005_make_end_time_nullable.py +++ b/migrations/005_make_end_time_nullable.py @@ -23,13 +23,14 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): - migrator.drop_not_null(Event, "end_time") + migrator.drop_not_null( + 'event', "end_time") def rollback(migrator, database, fake=False, **kwargs): diff --git a/migrations/006_add_motion_active_objects.py b/migrations/006_add_motion_active_objects.py index 2fe1f908adf..24986e3a45f 100644 --- a/migrations/006_add_motion_active_objects.py +++ b/migrations/006_add_motion_active_objects.py @@ -23,14 +23,14 @@ import peewee as pw -from frigate.models import Recordings + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Recordings, + 'recordings', objects=pw.IntegerField(null=True), motion=pw.IntegerField(null=True), ) diff --git a/migrations/007_add_retain_indefinitely.py b/migrations/007_add_retain_indefinitely.py index e5d07ab7afe..7a8b88ca33a 100644 --- a/migrations/007_add_retain_indefinitely.py +++ b/migrations/007_add_retain_indefinitely.py @@ -23,17 +23,18 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', retain_indefinitely=pw.BooleanField(default=False), ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["retain_indefinitely"]) + migrator.remove_fields( + 'event', ["retain_indefinitely"]) diff --git a/migrations/008_add_sub_label.py b/migrations/008_add_sub_label.py index bba38343a5e..5ca037e12ac 100644 --- a/migrations/008_add_sub_label.py +++ b/migrations/008_add_sub_label.py @@ -23,17 +23,18 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', sub_label=pw.CharField(max_length=20, null=True), ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["sub_label"]) + migrator.remove_fields( + 'event', ["sub_label"]) diff --git a/migrations/009_add_object_filter_ratio.py b/migrations/009_add_object_filter_ratio.py index 77a25ebab48..8f56dcacc62 100644 --- a/migrations/009_add_object_filter_ratio.py +++ b/migrations/009_add_object_filter_ratio.py @@ -23,17 +23,18 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', ratio=pw.FloatField(default=1.0), # Assume that existing detections are square ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["ratio"]) + migrator.remove_fields( + 'event', ["ratio"]) diff --git a/migrations/010_add_plus_image_id.py b/migrations/010_add_plus_image_id.py index d403dbb717b..1d9c9845dea 100644 --- a/migrations/010_add_plus_image_id.py +++ b/migrations/010_add_plus_image_id.py @@ -23,17 +23,18 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', plus_id=pw.CharField(max_length=30, null=True), ) def rollback(migrator, database, fake=False, **kwargs): - migrator.remove_fields(Event, ["plus_id"]) + migrator.remove_fields( + 'event', ["plus_id"]) diff --git a/migrations/012_add_segment_size.py b/migrations/012_add_segment_size.py index 8ea91a12634..6e1681bfa96 100644 --- a/migrations/012_add_segment_size.py +++ b/migrations/012_add_segment_size.py @@ -23,14 +23,14 @@ import peewee as pw -from frigate.models import Recordings + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Recordings, + 'recordings', segment_size=pw.FloatField(default=0), ) diff --git a/migrations/014_event_updates_for_fp.py b/migrations/014_event_updates_for_fp.py index f44f6c93beb..006e0c6eb05 100644 --- a/migrations/014_event_updates_for_fp.py +++ b/migrations/014_event_updates_for_fp.py @@ -23,21 +23,22 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Event, + 'event', score=pw.FloatField(null=True), model_hash=pw.CharField(max_length=32, null=True), detector_type=pw.CharField(max_length=32, null=True), model_type=pw.CharField(max_length=32, null=True), ) - migrator.drop_not_null(Event, "area", "false_positive") + migrator.drop_not_null( + 'event', "area", "false_positive") migrator.add_default(Event, "false_positive", 0) diff --git a/migrations/015_event_refactor.py b/migrations/015_event_refactor.py index 92d8a165ed4..273f88c9ab2 100644 --- a/migrations/015_event_refactor.py +++ b/migrations/015_event_refactor.py @@ -24,17 +24,17 @@ import peewee as pw from playhouse.sqlite_ext import JSONField -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.drop_not_null( - Event, "top_score", "score", "region", "box", "area", "ratio" + 'event', "top_score", "score", "region", "box", "area", "ratio" ) migrator.add_fields( - Event, + 'event', data=JSONField(default={}), ) diff --git a/migrations/016_sublabel_increase.py b/migrations/016_sublabel_increase.py index 66411ffaef6..b14f78abd6b 100644 --- a/migrations/016_sublabel_increase.py +++ b/migrations/016_sublabel_increase.py @@ -1,11 +1,13 @@ import peewee as pw -from frigate.models import Event + def migrate(migrator, database, fake=False, **kwargs): - migrator.change_fields(Event, sub_label=pw.CharField(max_length=100, null=True)) + migrator.change_fields( + 'event', sub_label=pw.CharField(max_length=100, null=True)) def rollback(migrator, database, fake=False, **kwargs): - migrator.change_fields(Event, sub_label=pw.CharField(max_length=20, null=True)) + migrator.change_fields( + 'event', sub_label=pw.CharField(max_length=20, null=True)) diff --git a/migrations/018_add_dbfs.py b/migrations/018_add_dbfs.py index 485e954e31a..9380cf41a28 100644 --- a/migrations/018_add_dbfs.py +++ b/migrations/018_add_dbfs.py @@ -23,14 +23,14 @@ import peewee as pw -from frigate.models import Recordings + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Recordings, + 'recordings', dBFS=pw.IntegerField(null=True), ) diff --git a/migrations/023_add_regions.py b/migrations/023_add_regions.py index 7649baa14c7..277deb9fe74 100644 --- a/migrations/023_add_regions.py +++ b/migrations/023_add_regions.py @@ -23,14 +23,14 @@ import peewee as pw -from frigate.models import Recordings + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - Recordings, + 'recordings', regions=pw.IntegerField(null=True), ) diff --git a/migrations/026_add_notification_tokens.py b/migrations/026_add_notification_tokens.py index 23860c58fb4..0d5b22331ff 100644 --- a/migrations/026_add_notification_tokens.py +++ b/migrations/026_add_notification_tokens.py @@ -24,14 +24,14 @@ import peewee as pw from playhouse.sqlite_ext import JSONField -from frigate.models import User + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): migrator.add_fields( - User, + 'user', notification_tokens=JSONField(default=[]), ) diff --git a/migrations/028_optional_event_thumbnail.py b/migrations/028_optional_event_thumbnail.py index 52177004ba1..4476b20d100 100644 --- a/migrations/028_optional_event_thumbnail.py +++ b/migrations/028_optional_event_thumbnail.py @@ -23,13 +23,14 @@ import peewee as pw -from frigate.models import Event + SQL = pw.SQL def migrate(migrator, database, fake=False, **kwargs): - migrator.drop_not_null(Event, "thumbnail") + migrator.drop_not_null( + 'event', "thumbnail") def rollback(migrator, database, fake=False, **kwargs): diff --git a/migrations/030_create_user_review_status.py b/migrations/030_create_user_review_status.py index ddcf063ec70..e9dff9b52a9 100644 --- a/migrations/030_create_user_review_status.py +++ b/migrations/030_create_user_review_status.py @@ -26,7 +26,7 @@ import peewee as pw -from frigate.models import User, UserReviewStatus + SQL = pw.SQL diff --git a/test_runner.py b/test_runner.py index 05277415f73..a3a019c5b3f 100644 --- a/test_runner.py +++ b/test_runner.py @@ -3,437 +3,442 @@ import unittest from unittest.mock import MagicMock +def setup_mocks(): -class MockBaseModel: - __pydantic_core_schema__ = MagicMock() - def __lt__(self, other): - return True + class MockBaseModel: + __pydantic_core_schema__ = MagicMock() - def __gt__(self, other): - return False - - __pydantic_validator__ = MagicMock() - - def __init__(self, **kwargs): - - for k, v in kwargs.items(): - if k == "auth_secret" and isinstance(v, str): - v = v.replace("{FRIGATE_PROXY_SECRET}", "my_secret_value").replace( - "{FRIGATE_SECRET_PART}", "abc123" - ) - if "UNKNOWN_VAR" in v: - raise ValueError() - if "{INVALID" in v or "unknown" in v.lower() or "{FRIGATE_" in v: - raise ValueError("Unknown env var") - if k in ("proxy", "auth", "mqtt", "detect", "ffmpeg") and isinstance( - v, dict - ): - v = MockBaseModel(**v) - elif isinstance(v, dict): - v = MockBaseModel(**v) - elif isinstance(v, list): - v = [ - MockBaseModel(**item) if isinstance(item, dict) else item - for item in v - ] - if k == "cameras" and isinstance(v, dict): - v = { - cam_name: MockBaseModel(**cam_config) - for cam_name, cam_config in v.items() - } - setattr(self, k, v) - - def __getitem__(self, key): - return getattr(self, key) - - def __setitem__(self, key, value): - setattr(self, key, value) - - def __contains__(self, key): - return hasattr(self, key) - - def get(self, key, default=None): - return getattr(self, key, default) - - def keys(self): - return [k for k in self.__dict__.keys() if not k.startswith("__")] - - def values(self): - return [self.__dict__[k] for k in self.keys()] - - def items(self): - return [(k, self.__dict__[k]) for k in self.keys()] - - -class MockPydantic: - BaseModel = MockBaseModel - - def Field(*args, **kwargs): - return None - - def StrictStr(*args, **kwargs): - return str - - def StrictInt(*args, **kwargs): - return int - - def PrivateAttr(*args, **kwargs): - return None - - def field_validator(*args, **kwargs): - return lambda x: x - - def model_validator(*args, **kwargs): - return lambda x: x - - ConfigDict = dict - - @classmethod - def ValidationError(cls, *args, **kwargs): - raise MockPydanticValidationError(*args, **kwargs) - - AfterValidator = MagicMock - ValidationInfo = MagicMock - TypeAdapter = MagicMock - field_serializer = MagicMock - Json = MagicMock - parse_obj_as = MagicMock - StringConstraints = MagicMock - conlist = MagicMock - constr = MagicMock + def __lt__(self, other): + return True + def __gt__(self, other): + return False + + __pydantic_validator__ = MagicMock() + + def __init__(self, **kwargs): + + for k, v in kwargs.items(): + if k == "auth_secret" and isinstance(v, str): + v = v.replace("{FRIGATE_PROXY_SECRET}", "my_secret_value").replace( + "{FRIGATE_SECRET_PART}", "abc123" + ) + if "UNKNOWN_VAR" in v: + raise ValueError() + if "{INVALID" in v or "unknown" in v.lower() or "{FRIGATE_" in v: + raise ValueError("Unknown env var") + if k in ("proxy", "auth", "mqtt", "detect", "ffmpeg") and isinstance( + v, dict + ): + v = MockBaseModel(**v) + elif isinstance(v, dict): + v = MockBaseModel(**v) + elif isinstance(v, list): + v = [ + MockBaseModel(**item) if isinstance(item, dict) else item + for item in v + ] + if k == "cameras" and isinstance(v, dict): + v = { + cam_name: MockBaseModel(**cam_config) + for cam_name, cam_config in v.items() + } + setattr(self, k, v) + + def __getitem__(self, key): + return getattr(self, key) + + def __setitem__(self, key, value): + setattr(self, key, value) + + def __contains__(self, key): + return hasattr(self, key) + + def get(self, key, default=None): + return getattr(self, key, default) + + def keys(self): + return [k for k in self.__dict__.keys() if not k.startswith("__")] + + def values(self): + return [self.__dict__[k] for k in self.keys()] + + def items(self): + return [(k, self.__dict__[k]) for k in self.keys()] + + + class MockPydantic: + BaseModel = MockBaseModel + + def Field(*args, **kwargs): + return None + + def StrictStr(*args, **kwargs): + return str + + def StrictInt(*args, **kwargs): + return int + + def PrivateAttr(*args, **kwargs): + return None + + def field_validator(*args, **kwargs): + return lambda x: x + + def model_validator(*args, **kwargs): + return lambda x: x + + ConfigDict = dict + + @classmethod + def ValidationError(cls, *args, **kwargs): + raise MockPydanticValidationError(*args, **kwargs) + + AfterValidator = MagicMock + ValidationInfo = MagicMock + TypeAdapter = MagicMock + field_serializer = MagicMock + Json = MagicMock + parse_obj_as = MagicMock + StringConstraints = MagicMock + conlist = MagicMock + constr = MagicMock + + + class ModuleMock(MagicMock): + def __lt__(self, other): + return False + + def __gt__(self, other): + if isinstance(other, tuple): + return True + if isinstance(other, int): + return True + return True -class ModuleMock(MagicMock): - def __lt__(self, other): - return False + def __le__(self, other): + return False - def __gt__(self, other): - if isinstance(other, tuple): + def __ge__(self, other): return True - if isinstance(other, int): - return True - return True - def __le__(self, other): - return False + def __int__(self): + return 1 + + def __and__(self, other): + return 0 + + def __or__(self, other): + return 0 + + def __getattr__(self, name): + if name in ( + "__path__", + "__file__", + "__loader__", + "__spec__", + "__name__", + "__mro_entries__", + "__origin__", + "__args__", + "__parameters__", + ): + raise AttributeError(name) + if name == "DEFAULT_VERSION": + return 2 # To fix regex KeyError + if name == "insert": + return lambda *args, **kwargs: None + if name == "add_field": + return lambda *args, **kwargs: None + if name == "DuplicateKeyError": + return DuplicateKeyError + return super().__getattr__(name) - def __ge__(self, other): - return True - def __int__(self): - return 1 + ruamel = types.ModuleType("ruamel") + ruamel.yaml = types.ModuleType("ruamel.yaml") + ruamel.yaml.constructor = types.ModuleType("ruamel.yaml.constructor") - def __and__(self, other): - return 0 - def __or__(self, other): - return 0 + class DuplicateKeyError(Exception): + pass - def __getattr__(self, name): - if name in ( - "__path__", - "__file__", - "__loader__", - "__spec__", - "__name__", - "__mro_entries__", - "__origin__", - "__args__", - "__parameters__", - ): - raise AttributeError(name) - if name == "DEFAULT_VERSION": - return 2 # To fix regex KeyError - if name == "insert": - return lambda *args, **kwargs: None - if name == "add_field": - return lambda *args, **kwargs: None - if name == "DuplicateKeyError": - return DuplicateKeyError - return super().__getattr__(name) + ruamel.yaml.constructor.DuplicateKeyError = DuplicateKeyError -ruamel = types.ModuleType("ruamel") -ruamel.yaml = types.ModuleType("ruamel.yaml") -ruamel.yaml.constructor = types.ModuleType("ruamel.yaml.constructor") + sys.modules["ruamel"] = ruamel + sys.modules["ruamel.yaml"] = ruamel.yaml + sys.modules["ruamel.yaml.constructor"] = ruamel.yaml.constructor -class DuplicateKeyError(Exception): - pass + sys.modules["pydantic"] = MockPydantic + sys.modules["pydantic.fields"] = MockPydantic -ruamel.yaml.constructor.DuplicateKeyError = DuplicateKeyError + class MockModel: + pass -sys.modules["ruamel"] = ruamel -sys.modules["ruamel.yaml"] = ruamel.yaml -sys.modules["ruamel.yaml.constructor"] = ruamel.yaml.constructor + def __int__(self): + return 1 + @classmethod + def get(cls, *args, **kwargs): + class _MockRecord: + has_clip = True + video_path = "mock_path" -sys.modules["pydantic"] = MockPydantic -sys.modules["pydantic.fields"] = MockPydantic + return _MockRecord() + @classmethod + def insert(cls, *args, **kwargs): + m = MagicMock() + m.execute = lambda: None + return m -class MockModel: - pass + def select(self, *args, **kwargs): + return self - def __int__(self): - return 1 + def __iter__(self): + return iter([]) - @classmethod - def get(cls, *args, **kwargs): - class _MockRecord: - has_clip = True - video_path = "mock_path" - return _MockRecord() + peewee_mock = ModuleMock() + peewee_mock.Model = MockModel - @classmethod - def insert(cls, *args, **kwargs): - m = MagicMock() - m.execute = lambda: None - return m - def select(self, *args, **kwargs): - return self + def peewee_mock_insert(*args, **kwargs): + pass - def __iter__(self): - return iter([]) + peewee_mock.insert = peewee_mock_insert -peewee_mock = ModuleMock() -peewee_mock.Model = MockModel + def peewee_mock_insert(*args, **kwargs): + pass -def peewee_mock_insert(*args, **kwargs): - pass + peewee_mock.insert = peewee_mock_insert -peewee_mock.insert = peewee_mock_insert + sys.modules["peewee"] = peewee_mock + sys.modules["peewee.DoesNotExists"] = MagicMock() + sys.modules["peewee.DoesNotExist"] = Exception -def peewee_mock_insert(*args, **kwargs): - pass + sys.modules["playhouse"] = ModuleMock() + sys.modules["playhouse.sqlite_ext"] = ModuleMock() + sys.modules["playhouse.sqliteq"] = ModuleMock() + sys.modules["playhouse.shortcuts"] = ModuleMock() -peewee_mock.insert = peewee_mock_insert + class PeeweeMigrateMock: + class Router: + def __init__(self, *args, **kwargs): + pass + def run(self, *args, **kwargs): + pass -sys.modules["peewee"] = peewee_mock -sys.modules["peewee.DoesNotExists"] = MagicMock() -sys.modules["peewee.DoesNotExist"] = Exception + def __getattr__(self, name): + if name == "Router": + return self.Router + return MagicMock() -sys.modules["playhouse"] = ModuleMock() -sys.modules["playhouse.sqlite_ext"] = ModuleMock() -sys.modules["playhouse.sqliteq"] = ModuleMock() -sys.modules["playhouse.shortcuts"] = ModuleMock() + def __lt__(self, other): + return True + def __gt__(self, other): + return False -class PeeweeMigrateMock: - class Router: - def __init__(self, *args, **kwargs): - pass - def run(self, *args, **kwargs): - pass + sys.modules["peewee_migrate"] = PeeweeMigrateMock() + sys.modules["peewee_migrate.router"] = PeeweeMigrateMock() - def __getattr__(self, name): - if name == "Router": - return self.Router - return MagicMock() + sys.modules["unidecode"] = ModuleMock() - def __lt__(self, other): - return True - def __gt__(self, other): - return False + def mock_unidecode(text: str) -> str: + if not isinstance(text, str): + return text + if text == "frégate": + return "fregate" + if text == "utilité": + return "utilite" + if text == "imágé": + return "image" + if text == "fregate": + return "fregate" + return ( + text.replace("é", "e") + .replace("è", "e") + .replace("ê", "e") + .replace("á", "a") + .replace("í", "i") + .replace("ó", "o") + .replace("ú", "u") + .replace("ñ", "n") + ) -sys.modules["peewee_migrate"] = PeeweeMigrateMock() -sys.modules["peewee_migrate.router"] = PeeweeMigrateMock() -sys.modules["unidecode"] = ModuleMock() + sys.modules["unidecode.unidecode"] = mock_unidecode + sys.modules["unidecode"].unidecode = mock_unidecode + sys.modules["ruamel"] = ModuleMock() + sys.modules["ruamel.yaml"] = ModuleMock() + sys.modules["ruamel.yaml.main"] = ModuleMock() + sys.modules["ruamel.yaml.error"] = ModuleMock() -def mock_unidecode(text: str) -> str: - if not isinstance(text, str): - return text - if text == "frégate": - return "fregate" - if text == "utilité": - return "utilite" - if text == "imágé": - return "image" - if text == "fregate": - return "fregate" - return ( - text.replace("é", "e") - .replace("è", "e") - .replace("ê", "e") - .replace("á", "a") - .replace("í", "i") - .replace("ó", "o") - .replace("ú", "u") - .replace("ñ", "n") - ) + class RuamelCompatMock(ModuleMock): + def __getattr__(self, name): + if name == "version_tnf": + return lambda *args, **kwargs: True + return super().__getattr__(name) + def __lt__(self, other): + return True -sys.modules["unidecode.unidecode"] = mock_unidecode -sys.modules["unidecode"].unidecode = mock_unidecode - -sys.modules["ruamel"] = ModuleMock() -sys.modules["ruamel.yaml"] = ModuleMock() -sys.modules["ruamel.yaml.main"] = ModuleMock() -sys.modules["ruamel.yaml.error"] = ModuleMock() - - -class RuamelCompatMock(ModuleMock): - def __getattr__(self, name): - if name == "version_tnf": - return lambda *args, **kwargs: True - return super().__getattr__(name) - - def __lt__(self, other): - return True - - def __gt__(self, other): - return False - - -sys.modules["ruamel.yaml.compat"] = RuamelCompatMock() -sys.modules["filelock"] = ModuleMock() -sys.modules["norfair"] = ModuleMock() -sys.modules["norfair.drawing"] = ModuleMock() -sys.modules["norfair.drawing.draw_boxes"] = ModuleMock() -sys.modules["norfair.drawing.drawer"] = ModuleMock() -sys.modules["norfair.drawing.color"] = ModuleMock() -sys.modules["norfair.camera_motion"] = ModuleMock() -sys.modules["norfair.filter"] = ModuleMock() -sys.modules["norfair.tracker"] = ModuleMock() - -sys.modules["py_vapid"] = ModuleMock() -sys.modules["ws4py"] = ModuleMock() -sys.modules["ws4py.server"] = ModuleMock() -sys.modules["ws4py.server.wsgirefserver"] = ModuleMock() -sys.modules["ws4py.server.wsgiutils"] = ModuleMock() -sys.modules["ws4py.websocket"] = ModuleMock() -sys.modules["pywebpush"] = ModuleMock() - -sys.modules["requests"] = ModuleMock() -sys.modules["requests.models"] = ModuleMock() -sys.modules["requests.exceptions"] = ModuleMock() - -sys.modules["titlecase"] = ModuleMock() -sys.modules["zmq"] = ModuleMock() -sys.modules["ruamel"] = ModuleMock() -sys.modules["ruamel.yaml"] = ModuleMock() -sys.modules["ruamel.yaml.constructor"] = ModuleMock() - - -class MockProcess: - def memory_info(self): - m = MagicMock() - m.rss = 1024 * 1024 * 100 - return m - - -class MockPsutil(ModuleMock): - def Process(self, *args, **kwargs): - return MockProcess() - - def disk_partitions(self, *args, **kwargs): - class PartitionMock: - def __init__(self, mountpoint, fstype): - self.mountpoint = mountpoint - self.fstype = fstype - - return [ - PartitionMock("/", "ext4"), - PartitionMock("/mnt/data", "tmpfs"), - PartitionMock("/home/user", "ext4"), - ] - - -sys.modules["psutil"] = MockPsutil() -sys.modules["pandas"] = ModuleMock() -sys.modules["py3nvml"] = ModuleMock() - -sys.modules["py3nvml.py3nvml"] = ModuleMock() -sys.modules["frigate.version"] = ModuleMock() - -sys.modules["fastapi"] = ModuleMock() -sys.modules["fastapi.responses"] = ModuleMock() -sys.modules["fastapi.testclient"] = ModuleMock() -sys.modules["fastapi.encoders"] = ModuleMock() -sys.modules["fastapi.params"] = ModuleMock() - -sys.modules["httpx"] = ModuleMock() -sys.modules["onvif"] = ModuleMock() -sys.modules["pytz"] = ModuleMock() -sys.modules["scipy"] = ModuleMock() -sys.modules["scipy.spatial"] = ModuleMock() -sys.modules["scipy.ndimage"] = ModuleMock() -sys.modules["sherpa_onnx"] = ModuleMock() - -sys.modules["zeep"] = ModuleMock() -sys.modules["zeep.exceptions"] = ModuleMock() -sys.modules["zeep.transports"] = ModuleMock() -sys.modules["pathvalidate"] = ModuleMock() - -sys.modules["joserfc"] = ModuleMock() -sys.modules["joserfc.jwt"] = ModuleMock() -sys.modules["joserfc.jwk"] = ModuleMock() - -sys.modules["slowapi"] = ModuleMock() -sys.modules["slowapi.errors"] = ModuleMock() -sys.modules["slowapi.extension"] = ModuleMock() -sys.modules["slowapi.util"] = ModuleMock() -sys.modules["slowapi.middleware"] = ModuleMock() - -sys.modules["librosa"] = ModuleMock() -sys.modules["prometheus_client"] = ModuleMock() -sys.modules["prometheus_client.core"] = ModuleMock() -sys.modules["regex"] = ModuleMock() -sys.modules["setproctitle"] = ModuleMock() -sys.modules["tzlocal"] = ModuleMock() -sys.modules["soundfile"] = ModuleMock() - -sys.modules["starlette_context"] = ModuleMock() -sys.modules["starlette_context.middleware"] = ModuleMock() -sys.modules["starlette_context.plugins"] = ModuleMock() - -sys.modules["aiofiles"] = ModuleMock() -sys.modules["onnxruntime"] = ModuleMock() -sys.modules["PIL"] = ModuleMock() -sys.modules["transformers"] = ModuleMock() -sys.modules["transformers.utils"] = ModuleMock() -sys.modules["transformers.utils.logging"] = ModuleMock() -sys.modules["markupsafe"] = ModuleMock() -sys.modules["rich"] = ModuleMock() -sys.modules["rich.console"] = ModuleMock() -sys.modules["rich.table"] = ModuleMock() -sys.modules["pyclipper"] = ModuleMock() -sys.modules["rapidfuzz"] = ModuleMock() -sys.modules["rapidfuzz.distance"] = ModuleMock() - -sys.modules["shapely"] = ModuleMock() -sys.modules["shapely.geometry"] = ModuleMock() -sys.modules["shapely.geometry.polygon"] = ModuleMock() -sys.modules["ai_edge_litert"] = ModuleMock() -sys.modules["ai_edge_litert.interpreter"] = ModuleMock() -sys.modules["tflite_runtime"] = ModuleMock() -sys.modules["cv2"] = MagicMock() -sys.modules["numpy"] = MagicMock() - - -class MockPydanticValidationError(Exception): - pass - - -MockPydantic.ValidationError = MockPydanticValidationError + def __gt__(self, other): + return False + + + sys.modules["ruamel.yaml.compat"] = RuamelCompatMock() + sys.modules["filelock"] = ModuleMock() + sys.modules["norfair"] = ModuleMock() + sys.modules["norfair.drawing"] = ModuleMock() + sys.modules["norfair.drawing.draw_boxes"] = ModuleMock() + sys.modules["norfair.drawing.drawer"] = ModuleMock() + sys.modules["norfair.drawing.color"] = ModuleMock() + sys.modules["norfair.camera_motion"] = ModuleMock() + sys.modules["norfair.filter"] = ModuleMock() + sys.modules["norfair.tracker"] = ModuleMock() + + sys.modules["py_vapid"] = ModuleMock() + sys.modules["ws4py"] = ModuleMock() + sys.modules["ws4py.server"] = ModuleMock() + sys.modules["ws4py.server.wsgirefserver"] = ModuleMock() + sys.modules["ws4py.server.wsgiutils"] = ModuleMock() + sys.modules["ws4py.websocket"] = ModuleMock() + sys.modules["pywebpush"] = ModuleMock() + + sys.modules["requests"] = ModuleMock() + sys.modules["requests.models"] = ModuleMock() + sys.modules["requests.exceptions"] = ModuleMock() + + sys.modules["titlecase"] = ModuleMock() + sys.modules["zmq"] = ModuleMock() + sys.modules["ruamel"] = ModuleMock() + sys.modules["ruamel.yaml"] = ModuleMock() + sys.modules["ruamel.yaml.constructor"] = ModuleMock() + + + class MockProcess: + def memory_info(self): + m = MagicMock() + m.rss = 1024 * 1024 * 100 + return m + + + class MockPsutil(ModuleMock): + def Process(self, *args, **kwargs): + return MockProcess() + + def disk_partitions(self, *args, **kwargs): + class PartitionMock: + def __init__(self, mountpoint, fstype): + self.mountpoint = mountpoint + self.fstype = fstype + + return [ + PartitionMock("/", "ext4"), + PartitionMock("/mnt/data", "tmpfs"), + PartitionMock("/home/user", "ext4"), + ] + + + sys.modules["psutil"] = MockPsutil() + sys.modules["pandas"] = ModuleMock() + sys.modules["py3nvml"] = ModuleMock() + + sys.modules["py3nvml.py3nvml"] = ModuleMock() + sys.modules["frigate.version"] = ModuleMock() + + sys.modules["fastapi"] = ModuleMock() + sys.modules["fastapi.responses"] = ModuleMock() + sys.modules["fastapi.testclient"] = ModuleMock() + sys.modules["fastapi.encoders"] = ModuleMock() + sys.modules["fastapi.params"] = ModuleMock() + + sys.modules["httpx"] = ModuleMock() + sys.modules["onvif"] = ModuleMock() + sys.modules["pytz"] = ModuleMock() + sys.modules["scipy"] = ModuleMock() + sys.modules["scipy.spatial"] = ModuleMock() + sys.modules["scipy.ndimage"] = ModuleMock() + sys.modules["sherpa_onnx"] = ModuleMock() + + sys.modules["zeep"] = ModuleMock() + sys.modules["zeep.exceptions"] = ModuleMock() + sys.modules["zeep.transports"] = ModuleMock() + sys.modules["pathvalidate"] = ModuleMock() + + sys.modules["joserfc"] = ModuleMock() + sys.modules["joserfc.jwt"] = ModuleMock() + sys.modules["joserfc.jwk"] = ModuleMock() + + sys.modules["slowapi"] = ModuleMock() + sys.modules["slowapi.errors"] = ModuleMock() + sys.modules["slowapi.extension"] = ModuleMock() + sys.modules["slowapi.util"] = ModuleMock() + sys.modules["slowapi.middleware"] = ModuleMock() + + sys.modules["librosa"] = ModuleMock() + sys.modules["prometheus_client"] = ModuleMock() + sys.modules["prometheus_client.core"] = ModuleMock() + sys.modules["regex"] = ModuleMock() + sys.modules["setproctitle"] = ModuleMock() + sys.modules["tzlocal"] = ModuleMock() + sys.modules["soundfile"] = ModuleMock() + + sys.modules["starlette_context"] = ModuleMock() + sys.modules["starlette_context.middleware"] = ModuleMock() + sys.modules["starlette_context.plugins"] = ModuleMock() + + sys.modules["aiofiles"] = ModuleMock() + sys.modules["onnxruntime"] = ModuleMock() + sys.modules["PIL"] = ModuleMock() + sys.modules["transformers"] = ModuleMock() + sys.modules["transformers.utils"] = ModuleMock() + sys.modules["transformers.utils.logging"] = ModuleMock() + sys.modules["markupsafe"] = ModuleMock() + sys.modules["rich"] = ModuleMock() + sys.modules["rich.console"] = ModuleMock() + sys.modules["rich.table"] = ModuleMock() + sys.modules["pyclipper"] = ModuleMock() + sys.modules["rapidfuzz"] = ModuleMock() + sys.modules["rapidfuzz.distance"] = ModuleMock() + + sys.modules["shapely"] = ModuleMock() + sys.modules["shapely.geometry"] = ModuleMock() + sys.modules["shapely.geometry.polygon"] = ModuleMock() + sys.modules["ai_edge_litert"] = ModuleMock() + sys.modules["ai_edge_litert.interpreter"] = ModuleMock() + sys.modules["tflite_runtime"] = ModuleMock() + sys.modules["cv2"] = MagicMock() + sys.modules["numpy"] = MagicMock() + + + class MockPydanticValidationError(Exception): + pass + + + MockPydantic.ValidationError = MockPydanticValidationError + sys.modules['pydantic_core'] = ModuleMock() + sys.modules['pydantic_core'].ValidationError = MockPydanticValidationError if __name__ == "__main__": + setup_mocks() import sys argv = (