mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-26 08:24:42 +03:00
122 lines
5.5 KiB
Python
122 lines
5.5 KiB
Python
"""Tests for motion search job limits."""
|
|||
|
|
|
||
|
|
from datetime import datetime
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
from frigate.jobs import motion_search
|
||
|
|
from frigate.models import Event, Recordings, ReviewSegment
|
||
|
|
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
|
||
|
|
|
||
|
|
URL = "/front_door/search/motion"
|
||
|
|
BODY = {
|
||
|
|
"start_time": 1000,
|
||
|
|
"end_time": 2000,
|
||
|
|
"polygon_points": [[0, 0], [1, 0], [1, 1]],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class TestHttpMotionSearch(BaseTestHttp):
|
||
|
|
def setUp(self):
|
||
|
|
super().setUp([Event, Recordings, ReviewSegment])
|
||
|
|
self.app = self.create_app()
|
||
|
|
|
||
|
|
motion_search._motion_search_jobs.clear()
|
||
|
|
self.addCleanup(motion_search._motion_search_jobs.clear)
|
||
|
|
|
||
|
|
runner_patch = patch("frigate.jobs.motion_search.MotionSearchRunner")
|
||
|
|
self.runner = runner_patch.start()
|
||
|
|
self.addCleanup(runner_patch.stop)
|
||
|
|
|
||
|
|
def start(self, client: AuthTestClient, user: str, body: dict = BODY):
|
||
|
|
return client.post(URL, json=body, headers={"remote-user": user})
|
||
|
|
|
||
|
|
def test_second_job_for_same_user_rejected(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 409)
|
||
|
|
self.assertEqual(self.start(client, "bob").status_code, 200)
|
||
|
|
|
||
|
|
def test_user_can_start_again_after_job_finishes(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
job_id = self.start(client, "alice").json()["job_id"]
|
||
|
|
job = motion_search.get_motion_search_job(job_id)
|
||
|
|
job.end_time = datetime.now().timestamp()
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 200)
|
||
|
|
self.assertIsNotNone(motion_search.get_motion_search_job(job_id))
|
||
|
|
|
||
|
|
def test_cancelled_job_frees_user_but_holds_global_slot_until_grace(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
job_id = self.start(client, "alice").json()["job_id"]
|
||
|
|
with patch("frigate.jobs.motion_search.InterProcessRequestor"):
|
||
|
|
self.assertTrue(motion_search.cancel_motion_search_job(job_id))
|
||
|
|
job = motion_search.get_motion_search_job(job_id)
|
||
|
|
self.assertIsNotNone(job.cancelled_at)
|
||
|
|
self.assertIsNone(job.end_time)
|
||
|
|
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "bob").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "carol").status_code, 429)
|
||
|
|
|
||
|
|
job.cancelled_at -= motion_search.CANCEL_GRACE_SECONDS + 1
|
||
|
|
self.assertEqual(self.start(client, "carol").status_code, 200)
|
||
|
|
|
||
|
|
def test_start_failure_releases_slot(self):
|
||
|
|
self.runner.return_value.start.side_effect = RuntimeError("boom")
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
with self.assertRaises(RuntimeError):
|
||
|
|
self.start(client, "alice")
|
||
|
|
self.assertEqual(motion_search._motion_search_jobs, {})
|
||
|
|
self.runner.return_value.requestor.stop.assert_called_once()
|
||
|
|
|
||
|
|
self.runner.return_value.start.side_effect = None
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 200)
|
||
|
|
|
||
|
|
def test_shared_identity_skips_per_user_limit(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
self.assertEqual(self.start(client, "anonymous").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "anonymous").status_code, 200)
|
||
|
|
|
||
|
|
motion_search._motion_search_jobs.clear()
|
||
|
|
self.minimal_config["auth"] = {"enabled": False}
|
||
|
|
app = self.create_app()
|
||
|
|
with AuthTestClient(app) as client:
|
||
|
|
self.assertEqual(self.start(client, "viewer").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "viewer").status_code, 200)
|
||
|
|
|
||
|
|
def test_proxy_user_header_keeps_per_user_limit_without_auth(self):
|
||
|
|
self.minimal_config["auth"] = {"enabled": False}
|
||
|
|
self.minimal_config["proxy"] = {"header_map": {"user": "x-forwarded-user"}}
|
||
|
|
app = self.create_app()
|
||
|
|
with AuthTestClient(app) as client:
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "alice").status_code, 409)
|
||
|
|
|
||
|
|
def test_global_cap_enforced(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
for i in range(motion_search.MAX_ACTIVE_JOBS):
|
||
|
|
self.assertEqual(self.start(client, f"user{i}").status_code, 200)
|
||
|
|
self.assertEqual(self.start(client, "late").status_code, 429)
|
||
|
|
|
||
|
|
def test_finished_jobs_evicted_after_ttl(self):
|
||
|
|
with AuthTestClient(self.app) as client:
|
||
|
|
old_id = self.start(client, "alice").json()["job_id"]
|
||
|
|
recent_id = self.start(client, "bob").json()["job_id"]
|
||
|
|
running_id = self.start(client, "carol").json()["job_id"]
|
||
|
|
|
||
|
|
now = 10_000.0
|
||
|
|
motion_search.get_motion_search_job(old_id).end_time = (
|
||
|
|
now - motion_search.FINISHED_JOB_TTL - 1
|
||
|
|
)
|
||
|
|
motion_search.get_motion_search_job(recent_id).end_time = now - 1
|
||
|
|
motion_search._evict_finished_jobs(now)
|
||
|
|
|
||
|
|
self.assertIsNone(motion_search.get_motion_search_job(old_id))
|
||
|
|
self.assertIsNotNone(motion_search.get_motion_search_job(recent_id))
|
||
|
|
self.assertIsNotNone(motion_search.get_motion_search_job(running_id))
|
||
|
|
|
||
|
|
def test_username_and_cancel_time_not_broadcast(self):
|
||
|
|
job = motion_search.MotionSearchJob(username="alice", cancelled_at=1.0)
|
||
|
|
payload = job.to_dict()
|
||
|
|
self.assertNotIn("username", payload)
|
||
|
|
self.assertNotIn("cancelled_at", payload)
|