mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-30 02:10:35 +03:00
Optimize Python Tests (#24823)
* Optimize tests * Address comments * Fix test
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "ghcr.io/devcontainers/features/common-utils@sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08",
|
||||
"integrity": "sha256:e76a6a8a35d1ad824daad1540725e0ec9f8ed2dc00d2850e990a9830013c7d08"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,4 +125,4 @@ jobs:
|
||||
- name: Check API spec is up to date
|
||||
run: devcontainer exec --workspace-folder . bash -lc "python3 generate_api_auth_spec.py --check"
|
||||
- name: Run unit tests in devcontainer
|
||||
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest"
|
||||
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m frigate.test"
|
||||
|
||||
@@ -36,7 +36,7 @@ When reviewing code, do NOT comment on:
|
||||
- **Formatting**: Ruff (configured in `pyproject.toml`)
|
||||
- **Linting**: Ruff with rules defined in project config
|
||||
- **Type Checking**: Use type hints consistently
|
||||
- **Testing**: unittest framework - use `python3 -u -m unittest` to run tests
|
||||
- **Testing**: unittest framework - use `python3 -u -m frigate.test` to run the suite in parallel groups
|
||||
- **Language**: American English for all code, comments, and documentation
|
||||
- **Punctuation**: Do not use em dashes in documentation, comments, or strings; reword with standard punctuation (commas, colons, parentheses, or separate sentences)
|
||||
|
||||
@@ -196,7 +196,7 @@ Key rules enforced:
|
||||
### Backend Testing
|
||||
|
||||
- **Framework**: Python unittest
|
||||
- **Run Command**: `python3 -u -m unittest`
|
||||
- **Run Command**: `python3 -u -m frigate.test` (parallel groups; plain `python3 -u -m unittest` runs serially)
|
||||
- **Location**: `frigate/test/`
|
||||
- **Coverage**: Aim for comprehensive test coverage of core functionality
|
||||
- **Pattern**: Use `TestCase` classes with descriptive test method names
|
||||
@@ -219,8 +219,8 @@ Key rules enforced:
|
||||
### Python Backend
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
python3 -u -m unittest
|
||||
# Run all tests in parallel groups (-j to set the number of jobs)
|
||||
python3 -u -m frigate.test
|
||||
|
||||
# Run specific test file
|
||||
python3 -u -m unittest frigate.test.test_ffmpeg_presets
|
||||
|
||||
+2
-2
@@ -64,7 +64,7 @@ Pull requests that appear to be unreviewed AI output will be closed without revi
|
||||
- **Python** — use modern language features (type hints, pattern matching, f-strings, dataclasses)
|
||||
- **Formatting**: Ruff (configured in `pyproject.toml`)
|
||||
- **Linting**: Ruff
|
||||
- **Testing**: `python3 -u -m unittest`
|
||||
- **Testing**: `python3 -u -m frigate.test` (runs the suite in parallel groups)
|
||||
- **Logging**: Use module-level `logger = logging.getLogger(__name__)` with lazy formatting
|
||||
- **Async**: All external I/O must be async. No blocking calls in async functions.
|
||||
- **Error handling**: Use specific exception types. Keep try blocks minimal.
|
||||
@@ -82,7 +82,7 @@ Pull requests that appear to be unreviewed AI output will be closed without revi
|
||||
|
||||
```bash
|
||||
# Python
|
||||
python3 -u -m unittest # Run all tests
|
||||
python3 -u -m frigate.test # Run all tests in parallel groups
|
||||
python3 -u -m unittest frigate.test.test_ffmpeg_presets # Run specific test
|
||||
ruff format frigate # Format
|
||||
ruff check frigate # Lint
|
||||
|
||||
@@ -54,7 +54,7 @@ run: local
|
||||
|
||||
run_tests: local
|
||||
docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \
|
||||
python3 -u -m unittest
|
||||
python3 -u -m frigate.test
|
||||
docker run --rm --workdir=/opt/frigate --entrypoint= frigate:latest \
|
||||
python3 -u -m mypy --config-file frigate/mypy.ini frigate
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ After closing VS Code, you may still have containers running. To close everythin
|
||||
GitHub will execute unit tests on new PRs. You must ensure that all tests pass.
|
||||
|
||||
```shell
|
||||
python3 -u -m unittest
|
||||
python3 -u -m frigate.test
|
||||
```
|
||||
|
||||
#### FFMPEG Hardware Acceleration
|
||||
@@ -207,7 +207,7 @@ npm run lint
|
||||
- Ensure the backend [unit tests](#unit-tests) pass. Your PR cannot be merged unless tests pass.
|
||||
|
||||
```shell
|
||||
python3 -u -m unittest
|
||||
python3 -u -m frigate.test
|
||||
```
|
||||
|
||||
- Ensure the end-to-end tests pass. They run in Playwright against a production build with mocked API data, so they don't need a running Frigate instance. Add or update tests in `web/e2e/specs/` when you change UI behavior.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from frigate.config.config import stream_info_retriever
|
||||
|
||||
|
||||
def _unreachable_stream_info(ffmpeg, path: str) -> dict:
|
||||
return {"width": 0, "height": 0, "fourcc": None}
|
||||
|
||||
|
||||
# Validating a camera without detect dimensions probes its stream. Test streams
|
||||
# are unreachable, so return the failed probe result instead of waiting ~12s per
|
||||
# stream for ffprobe to time out
|
||||
stream_info_retriever.get_stream_info = _unreachable_stream_info
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
"""Run the unit test suite in parallel groups.
|
||||
|
||||
Test modules are split into groups balanced by test count, similar to Home
|
||||
Assistant's split_tests.py, and each group runs in its own unittest process.
|
||||
|
||||
Usage (from the repo root):
|
||||
python3 -u -m frigate.test [-j JOBS] [--groups GROUPS] [--timeout SECONDS]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
TEST_METHOD = re.compile(r"^\s+(?:async )?def test", re.MULTILINE)
|
||||
RAN_TESTS = re.compile(r"^Ran (\d+) tests? in", re.MULTILINE)
|
||||
|
||||
# Each group pays for its own interpreter and imports, so smaller groups only
|
||||
# help when there are more cores than groups
|
||||
TESTS_PER_GROUP = 250
|
||||
|
||||
# Each group runs in its own process group, which ctrl+c in the terminal does
|
||||
# not reach, so the runner passes the interrupt on to these
|
||||
interrupted = threading.Event()
|
||||
running_groups: set[int] = set()
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestGroup:
|
||||
index: int
|
||||
modules: list[str] = field(default_factory=list)
|
||||
weight: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroupResult:
|
||||
group: TestGroup
|
||||
returncode: int
|
||||
output: str
|
||||
duration: float
|
||||
|
||||
@property
|
||||
def tests_run(self) -> int:
|
||||
"""Number of tests unittest reported running, 0 if it never finished."""
|
||||
match = RAN_TESTS.search(self.output)
|
||||
return int(match.group(1)) if match else 0
|
||||
|
||||
|
||||
def discover_modules() -> dict[str, int]:
|
||||
"""Find test modules the same way unittest discovery does.
|
||||
|
||||
Returns:
|
||||
Module name mapped to the number of test methods it defines
|
||||
"""
|
||||
modules: dict[str, int] = {}
|
||||
|
||||
for path in sorted((REPO_ROOT / "frigate").rglob("test*.py")):
|
||||
relative = path.relative_to(REPO_ROOT)
|
||||
|
||||
# discovery only descends into packages
|
||||
if not all(
|
||||
(REPO_ROOT / parent / "__init__.py").exists()
|
||||
for parent in relative.parents
|
||||
if parent != Path(".")
|
||||
):
|
||||
continue
|
||||
|
||||
name = ".".join(relative.with_suffix("").parts)
|
||||
modules[name] = len(TEST_METHOD.findall(path.read_text()))
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def split_groups(modules: dict[str, int], count: int) -> list[TestGroup]:
|
||||
"""Split modules into groups with a similar number of tests.
|
||||
|
||||
Largest modules are placed first, each into the currently lightest group.
|
||||
"""
|
||||
groups = [TestGroup(index) for index in range(1, count + 1)]
|
||||
|
||||
for name, weight in sorted(modules.items(), key=lambda m: m[1], reverse=True):
|
||||
group = min(groups, key=lambda g: g.weight)
|
||||
group.modules.append(name)
|
||||
group.weight += weight
|
||||
|
||||
return [group for group in groups if group.modules]
|
||||
|
||||
|
||||
def signal_group(pgid: int, sig: signal.Signals) -> bool:
|
||||
"""Send a signal to every process in a process group.
|
||||
|
||||
Returns:
|
||||
False if no process is left in the group
|
||||
"""
|
||||
try:
|
||||
os.killpg(pgid, sig)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stop_group(pgid: int) -> None:
|
||||
"""Stop the processes a group's tests left running.
|
||||
|
||||
They get a moment to exit before being killed, so the multiprocessing
|
||||
resource tracker, which ignores SIGTERM, can remove leaked shared memory
|
||||
once the processes holding it have exited.
|
||||
"""
|
||||
if signal_group(pgid, signal.SIGTERM):
|
||||
time.sleep(2)
|
||||
signal_group(pgid, signal.SIGKILL)
|
||||
|
||||
|
||||
def run_group(group: TestGroup, timeout: float) -> GroupResult:
|
||||
"""Run a group's modules in a unittest process of its own.
|
||||
|
||||
Args:
|
||||
group: The modules to run
|
||||
timeout: Seconds before the group is stopped
|
||||
|
||||
Returns:
|
||||
The exit code and combined output of the unittest process
|
||||
"""
|
||||
start = time.monotonic()
|
||||
|
||||
# a file instead of a pipe so a process left behind by a test can't keep
|
||||
# the group from finishing by holding the pipe open
|
||||
with tempfile.TemporaryFile("w+") as output:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-u", "-X", "faulthandler", "-m", "unittest"]
|
||||
+ group.modules,
|
||||
cwd=REPO_ROOT,
|
||||
# a group in the background that read the terminal would be
|
||||
# stopped, and a stopped process doesn't act on SIGABRT
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=output,
|
||||
stderr=subprocess.STDOUT,
|
||||
# a process group of its own so the processes its tests start can
|
||||
# be stopped with it
|
||||
process_group=0,
|
||||
)
|
||||
running_groups.add(proc.pid)
|
||||
|
||||
# the runner may have been interrupted while this group was starting
|
||||
if interrupted.is_set():
|
||||
signal_group(proc.pid, signal.SIGINT)
|
||||
|
||||
timed_out = False
|
||||
|
||||
try:
|
||||
proc.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
# faulthandler prints the stack of every thread on SIGABRT, which
|
||||
# shows where the group is stuck
|
||||
timed_out = True
|
||||
proc.send_signal(signal.SIGABRT)
|
||||
proc.wait()
|
||||
|
||||
# aborting skips the cleanup of the running test, which would
|
||||
# have stopped the processes it started
|
||||
stop_group(proc.pid)
|
||||
|
||||
running_groups.discard(proc.pid)
|
||||
output.seek(0)
|
||||
text = output.read()
|
||||
|
||||
if timed_out:
|
||||
text += f"\nGroup timed out after {timeout:.0f}s\n"
|
||||
|
||||
return GroupResult(group, proc.returncode, text, time.monotonic() - start)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Split the tests into groups, run them in parallel, and report the results.
|
||||
|
||||
Returns:
|
||||
The exit code, 1 if any group failed
|
||||
"""
|
||||
cpus = os.cpu_count() or 1
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
type=int,
|
||||
default=cpus,
|
||||
help=f"number of groups to run at once (default: {cpus})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--groups",
|
||||
type=int,
|
||||
help=(
|
||||
"number of groups to split the tests into (default: one per job, "
|
||||
f"or more to keep groups under about {TESTS_PER_GROUP} tests)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timeout",
|
||||
type=float,
|
||||
default=600,
|
||||
help="seconds before a group is stopped and reported as failed",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="print the output of passing groups as well as failing ones",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
modules = discover_modules()
|
||||
group_count = args.groups or max(
|
||||
args.jobs, math.ceil(sum(modules.values()) / TESTS_PER_GROUP)
|
||||
)
|
||||
groups = split_groups(modules, group_count)
|
||||
print(f"Running {len(groups)} test groups with {args.jobs} jobs", flush=True)
|
||||
|
||||
start = time.monotonic()
|
||||
failed: list[GroupResult] = []
|
||||
total_tests = 0
|
||||
|
||||
with ThreadPoolExecutor(max_workers=args.jobs) as executor:
|
||||
futures = [executor.submit(run_group, group, args.timeout) for group in groups]
|
||||
|
||||
try:
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
total_tests += result.tests_run
|
||||
status = "OK" if result.returncode == 0 else "FAILED"
|
||||
print(
|
||||
f"Group {result.group.index}/{len(groups)}: {status}, "
|
||||
f"{result.tests_run} tests in {result.duration:.1f}s",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
failed.append(result)
|
||||
|
||||
if result.returncode != 0 or args.verbose:
|
||||
print(result.output, flush=True)
|
||||
except KeyboardInterrupt:
|
||||
interrupted.set()
|
||||
executor.shutdown(wait=False, cancel_futures=True)
|
||||
|
||||
for pgid in list(running_groups):
|
||||
signal_group(pgid, signal.SIGINT)
|
||||
|
||||
raise
|
||||
|
||||
print(f"\nRan {total_tests} tests in {time.monotonic() - start:.1f}s")
|
||||
|
||||
if failed:
|
||||
print(f"{len(failed)} of {len(groups)} groups FAILED, rerun one with:")
|
||||
|
||||
for result in sorted(failed, key=lambda r: r.group.index):
|
||||
print(
|
||||
f" Group {result.group.index}: python3 -u -m unittest "
|
||||
f"{' '.join(result.group.modules)}"
|
||||
)
|
||||
|
||||
return 1
|
||||
|
||||
print("OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+10
-2
@@ -1,4 +1,12 @@
|
||||
"""Constants for testing."""
|
||||
|
||||
TEST_DB = "test.db"
|
||||
TEST_DB_CLEANUPS = ["test.db", "test.db-shm", "test.db-wal"]
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
# Unique per process so test groups can run in parallel without sharing a database
|
||||
TEST_DB = os.path.join(tempfile.gettempdir(), f"frigate_test_{os.getpid()}.db")
|
||||
TEST_DB_CLEANUPS = [TEST_DB, f"{TEST_DB}-shm", f"{TEST_DB}-wal"]
|
||||
|
||||
# The database is thrown away after each test, so skip syncing every commit to
|
||||
# disk, which is slow when several test processes are writing at once
|
||||
TEST_DB_PRAGMAS = {"synchronous": "off"}
|
||||
|
||||
@@ -18,7 +18,7 @@ from frigate.debug_replay import DebugReplayManager
|
||||
from frigate.jobs.export import JobStatePublisher
|
||||
from frigate.models import Event, Recordings, ReviewSegment
|
||||
from frigate.review.types import SeverityEnum
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS, TEST_DB_PRAGMAS
|
||||
|
||||
|
||||
class AuthTestClient(TestClient):
|
||||
@@ -38,12 +38,12 @@ class AuthTestClient(TestClient):
|
||||
class BaseTestHttp(unittest.TestCase):
|
||||
def setUp(self, models):
|
||||
# setup clean database for each test run
|
||||
migrate_db = SqliteDatabase("test.db")
|
||||
migrate_db = SqliteDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
migrate_db.close()
|
||||
self.db = SqliteQueueDatabase(TEST_DB)
|
||||
self.db = SqliteQueueDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
self.db.bind(models)
|
||||
|
||||
# The export job manager broadcasts via JobStatePublisher on
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import os
|
||||
import shutil
|
||||
from unittest.mock import MagicMock
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from frigate.output.preview import PREVIEW_CACHE_DIR, PREVIEW_FRAME_TYPE
|
||||
from frigate.output.preview import PREVIEW_FRAME_TYPE
|
||||
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
||||
|
||||
|
||||
@@ -15,14 +16,15 @@ class TestHttpLatestFrame(BaseTestHttp):
|
||||
self.app = super().create_app()
|
||||
self.app.detected_frames_processor = MagicMock()
|
||||
|
||||
if os.path.exists(PREVIEW_CACHE_DIR):
|
||||
shutil.rmtree(PREVIEW_CACHE_DIR)
|
||||
os.makedirs(PREVIEW_CACHE_DIR)
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(PREVIEW_CACHE_DIR):
|
||||
shutil.rmtree(PREVIEW_CACHE_DIR)
|
||||
super().tearDown()
|
||||
# a directory of its own so test groups running in parallel can't
|
||||
# delete each other's preview frames
|
||||
self.preview_dir = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, self.preview_dir, ignore_errors=True)
|
||||
cache_dir_patch = patch(
|
||||
"frigate.output.preview.PREVIEW_CACHE_DIR", self.preview_dir
|
||||
)
|
||||
cache_dir_patch.start()
|
||||
self.addCleanup(cache_dir_patch.stop)
|
||||
|
||||
def test_latest_frame_fallback_to_preview(self):
|
||||
camera = "front_door"
|
||||
@@ -45,7 +47,7 @@ class TestHttpLatestFrame(BaseTestHttp):
|
||||
2,
|
||||
)
|
||||
preview_path = os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-1234567890.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-1234567890.0.{PREVIEW_FRAME_TYPE}"
|
||||
)
|
||||
cv2.imwrite(preview_path, dummy_frame)
|
||||
|
||||
@@ -80,7 +82,7 @@ class TestHttpLatestFrame(BaseTestHttp):
|
||||
|
||||
# 2. Create a dummy preview file
|
||||
preview_path = os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-999.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-999.0.{PREVIEW_FRAME_TYPE}"
|
||||
)
|
||||
cv2.imwrite(preview_path, dummy_frame)
|
||||
|
||||
|
||||
@@ -617,9 +617,13 @@ class TestExportTimelineAlignment(unittest.TestCase):
|
||||
):
|
||||
windows = exporter._staged_chapter_windows()
|
||||
|
||||
path = exporter._build_recording_segment_chapter_metadata_file(windows)
|
||||
self.addCleanup(os.remove, path)
|
||||
content = Path(path).read_text()
|
||||
# the real cache dir only exists once the container has started
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
chapter_path = os.path.join(tmpdir, "chapters.txt")
|
||||
exporter._chapter_metadata_path = lambda: chapter_path # type: ignore[method-assign]
|
||||
path = exporter._build_recording_segment_chapter_metadata_file(windows)
|
||||
self.assertEqual(path, chapter_path)
|
||||
content = Path(chapter_path).read_text()
|
||||
|
||||
self.assertIn("START=0\nEND=21500", content)
|
||||
self.assertIn("START=21500\nEND=43000", content)
|
||||
|
||||
@@ -21,7 +21,7 @@ from frigate.api.media_auth import (
|
||||
)
|
||||
from frigate.config import FrigateConfig
|
||||
from frigate.models import Event, Export, Recordings, ReviewSegment
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS, TEST_DB_PRAGMAS
|
||||
|
||||
_CONFIG = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
@@ -222,11 +222,11 @@ class TestExportResolution(unittest.TestCase):
|
||||
"""Export resolution requires a DB lookup."""
|
||||
|
||||
def setUp(self):
|
||||
migrate_db = SqliteDatabase("test.db")
|
||||
migrate_db = SqliteDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
Router(migrate_db).run()
|
||||
migrate_db.close()
|
||||
self.db = SqliteQueueDatabase(TEST_DB)
|
||||
self.db = SqliteQueueDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
self.db.bind([Event, ReviewSegment, Recordings, Export])
|
||||
self.config = FrigateConfig(**_CONFIG)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from playhouse.sqliteq import SqliteQueueDatabase
|
||||
from frigate.models import Notice, NoticeStats
|
||||
from frigate.notices.registry import NoticeRegistry
|
||||
from frigate.notices.types import NOTICE_KINDS, NoticeKind, NoticeSeverity, notice_id
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS, TEST_DB_PRAGMAS
|
||||
|
||||
# kinds that switch on the lifecycle knobs, so the tests do not depend on the
|
||||
# values the real catalog picks
|
||||
@@ -49,12 +49,12 @@ class TestNoticeKinds(unittest.TestCase):
|
||||
|
||||
class RegistryTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
migrate_db = SqliteDatabase("test.db")
|
||||
migrate_db = SqliteDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
migrate_db.close()
|
||||
self.db = SqliteQueueDatabase(TEST_DB)
|
||||
self.db = SqliteQueueDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
self.db.bind([Notice, NoticeStats])
|
||||
self.registry = NoticeRegistry()
|
||||
self.listener = MagicMock()
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from frigate.output.preview import (
|
||||
PREVIEW_CACHE_DIR,
|
||||
PREVIEW_FRAME_TYPE,
|
||||
get_most_recent_preview_frame,
|
||||
)
|
||||
from frigate.output.preview import PREVIEW_FRAME_TYPE, get_most_recent_preview_frame
|
||||
|
||||
|
||||
class TestPreviewLoader(unittest.TestCase):
|
||||
def setUp(self):
|
||||
if os.path.exists(PREVIEW_CACHE_DIR):
|
||||
shutil.rmtree(PREVIEW_CACHE_DIR)
|
||||
os.makedirs(PREVIEW_CACHE_DIR)
|
||||
|
||||
def tearDown(self):
|
||||
if os.path.exists(PREVIEW_CACHE_DIR):
|
||||
shutil.rmtree(PREVIEW_CACHE_DIR)
|
||||
# a directory of its own so test groups running in parallel can't
|
||||
# delete each other's preview frames
|
||||
self.preview_dir = tempfile.mkdtemp()
|
||||
self.addCleanup(shutil.rmtree, self.preview_dir, ignore_errors=True)
|
||||
cache_dir_patch = patch(
|
||||
"frigate.output.preview.PREVIEW_CACHE_DIR", self.preview_dir
|
||||
)
|
||||
cache_dir_patch.start()
|
||||
self.addCleanup(cache_dir_patch.stop)
|
||||
|
||||
def test_get_most_recent_preview_frame_missing(self):
|
||||
self.assertIsNone(get_most_recent_preview_frame("test_camera"))
|
||||
@@ -28,14 +28,14 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
for ts in ["1000.0", "2000.0", "1500.0"]:
|
||||
with open(
|
||||
os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}"
|
||||
),
|
||||
"w",
|
||||
) as f:
|
||||
f.write(f"test_{ts}")
|
||||
|
||||
expected_path = os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-2000.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-2000.0.{PREVIEW_FRAME_TYPE}"
|
||||
)
|
||||
self.assertEqual(get_most_recent_preview_frame(camera), expected_path)
|
||||
|
||||
@@ -45,7 +45,7 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
for ts in ["1000.0", "2000.0"]:
|
||||
with open(
|
||||
os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-{ts}.{PREVIEW_FRAME_TYPE}"
|
||||
),
|
||||
"w",
|
||||
) as f:
|
||||
@@ -53,7 +53,7 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
|
||||
# Test finding frame before or at 1500
|
||||
expected_path = os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{camera}-1000.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{camera}-1000.0.{PREVIEW_FRAME_TYPE}"
|
||||
)
|
||||
self.assertEqual(
|
||||
get_most_recent_preview_frame(camera, before=1500.0), expected_path
|
||||
@@ -67,7 +67,7 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
other_camera = "other_camera"
|
||||
with open(
|
||||
os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_{other_camera}-3000.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_{other_camera}-3000.0.{PREVIEW_FRAME_TYPE}"
|
||||
),
|
||||
"w",
|
||||
) as f:
|
||||
@@ -78,12 +78,12 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
def test_get_most_recent_preview_frame_hyphenated_camera(self):
|
||||
for name in ("preview_front-2000.0", "preview_front-door-3000.0"):
|
||||
with open(
|
||||
os.path.join(PREVIEW_CACHE_DIR, f"{name}.{PREVIEW_FRAME_TYPE}"), "w"
|
||||
os.path.join(self.preview_dir, f"{name}.{PREVIEW_FRAME_TYPE}"), "w"
|
||||
) as f:
|
||||
f.write("test")
|
||||
|
||||
expected_path = os.path.join(
|
||||
PREVIEW_CACHE_DIR, f"preview_front-2000.0.{PREVIEW_FRAME_TYPE}"
|
||||
self.preview_dir, f"preview_front-2000.0.{PREVIEW_FRAME_TYPE}"
|
||||
)
|
||||
self.assertEqual(get_most_recent_preview_frame("front"), expected_path)
|
||||
self.assertEqual(
|
||||
@@ -91,5 +91,5 @@ class TestPreviewLoader(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_get_most_recent_preview_frame_no_directory(self):
|
||||
shutil.rmtree(PREVIEW_CACHE_DIR)
|
||||
shutil.rmtree(self.preview_dir)
|
||||
self.assertIsNone(get_most_recent_preview_frame("test_camera"))
|
||||
|
||||
@@ -13,22 +13,28 @@ from frigate.config import FrigateConfig
|
||||
from frigate.const import STREAM_TYPE_MAIN, STREAM_TYPE_SUB
|
||||
from frigate.models import Event, Recordings
|
||||
from frigate.storage import MAX_CALCULATED_BANDWIDTH, StorageMaintainer
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS
|
||||
from frigate.test.const import TEST_DB, TEST_DB_CLEANUPS, TEST_DB_PRAGMAS
|
||||
|
||||
|
||||
class TestHttp(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# setup clean database for each test run
|
||||
migrate_db = SqliteDatabase("test.db")
|
||||
migrate_db = SqliteDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
del logging.getLogger("peewee_migrate").handlers[:]
|
||||
router = Router(migrate_db)
|
||||
router.run()
|
||||
migrate_db.close()
|
||||
self.db = SqliteQueueDatabase(TEST_DB)
|
||||
self.db = SqliteQueueDatabase(TEST_DB, pragmas=TEST_DB_PRAGMAS)
|
||||
models = [Event, Recordings]
|
||||
self.db.bind(models)
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
|
||||
# The maintainers are never stopped, so a real subscriber leaks a zmq
|
||||
# context whose garbage collection can hang a later test in term()
|
||||
subscriber_patch = patch("frigate.storage.CameraConfigUpdateSubscriber")
|
||||
subscriber_patch.start()
|
||||
self.addCleanup(subscriber_patch.stop)
|
||||
|
||||
self.minimal_config = {
|
||||
"mqtt": {"host": "mqtt"},
|
||||
"cameras": {
|
||||
|
||||
Reference in New Issue
Block a user