Add tests for camera manager cache.
This commit is contained in:
Vendored
+2
-1
@@ -4,5 +4,6 @@
|
|||||||
"i18n-ally.sortKeys": true,
|
"i18n-ally.sortKeys": true,
|
||||||
"i18n-ally.keepFulfilled": true,
|
"i18n-ally.keepFulfilled": true,
|
||||||
"i18n-ally.editor.preferEditor": true,
|
"i18n-ally.editor.preferEditor": true,
|
||||||
"i18n-ally.translate.saveAsCandidates": true
|
"i18n-ally.translate.saveAsCandidates": true,
|
||||||
|
"vitest.commandLine": "npx vitest --root /home/dizer/src/frigate-hass-card"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ export class MemoryRequestCache<Request, Response>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._data = [];
|
||||||
|
}
|
||||||
|
|
||||||
public has(request: Request): boolean {
|
public has(request: Request): boolean {
|
||||||
return !!this.get(request);
|
return !!this.get(request);
|
||||||
}
|
}
|
||||||
@@ -92,7 +96,7 @@ class MemoryRangedCache<Data> {
|
|||||||
const output: Data[] = [];
|
const output: Data[] = [];
|
||||||
for (const data of this._data) {
|
for (const data of this._data) {
|
||||||
const start = this._timeFunc(data);
|
const start = this._timeFunc(data);
|
||||||
if (start > range.start.getTime()) {
|
if (start >= range.start.getTime()) {
|
||||||
if (start > range.end.getTime()) {
|
if (start > range.end.getTime()) {
|
||||||
// Data is kept in order.
|
// Data is kept in order.
|
||||||
break;
|
break;
|
||||||
@@ -103,10 +107,6 @@ class MemoryRangedCache<Data> {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
public size(): number {
|
|
||||||
return this._data.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove old data that matches a given predicate. No change to the covered
|
* Remove old data that matches a given predicate. No change to the covered
|
||||||
* ranges is made, i.e. this is asserting authoritiatively that this data does
|
* ranges is made, i.e. this is asserting authoritiatively that this data does
|
||||||
@@ -114,7 +114,7 @@ class MemoryRangedCache<Data> {
|
|||||||
* @param predicate A predicate to run on each data element.
|
* @param predicate A predicate to run on each data element.
|
||||||
*/
|
*/
|
||||||
public expireMatches(predicate: (data: Data) => boolean): void {
|
public expireMatches(predicate: (data: Data) => boolean): void {
|
||||||
this._data = this._data.filter(predicate);
|
this._data = this._data.filter((data) => !predicate(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +134,10 @@ export class RecordingSegmentsCache {
|
|||||||
cameraSegmentCache.add(range, segments);
|
cameraSegmentCache.add(range, segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._segments.clear();
|
||||||
|
}
|
||||||
|
|
||||||
public hasCoverage(cameraID: string, range: DateRange): boolean {
|
public hasCoverage(cameraID: string, range: DateRange): boolean {
|
||||||
return !!this._segments.get(cameraID)?.hasCoverage(range);
|
return !!this._segments.get(cameraID)?.hasCoverage(range);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1025,7 +1025,7 @@ export class FrigateCameraManagerEngine
|
|||||||
(segment: RecordingSegment) => {
|
(segment: RecordingSegment) => {
|
||||||
const hourID = getHourID(cameraID, fromUnixTime(segment.start_time));
|
const hourID = getHourID(cameraID, fromUnixTime(segment.start_time));
|
||||||
// ~O(1) lookup time for a JS set.
|
// ~O(1) lookup time for a JS set.
|
||||||
return goodHours.has(hourID);
|
return !goodHours.has(hourID);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import add from 'date-fns/add';
|
||||||
|
import sub from 'date-fns/sub';
|
||||||
|
import sortBy from 'lodash-es/sortBy';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import {
|
||||||
|
MemoryRequestCache,
|
||||||
|
RecordingSegmentsCache,
|
||||||
|
} from '../../src/camera-manager/cache.js';
|
||||||
|
import { DateRange } from '../../src/camera-manager/range.js';
|
||||||
|
import { RecordingSegment } from '../../src/camera-manager/types.js';
|
||||||
|
|
||||||
|
describe('MemoryRequestCache', () => {
|
||||||
|
const cache = new MemoryRequestCache();
|
||||||
|
const request = { request: 'foo' };
|
||||||
|
const response = { response: 'bar' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cache.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get value when set', () => {
|
||||||
|
cache.set(request, response);
|
||||||
|
expect(cache.get(request)).toBe(response);
|
||||||
|
});
|
||||||
|
it('should get similar value when set', () => {
|
||||||
|
cache.set(request, response);
|
||||||
|
expect(cache.get({ ...request })).toBe(response);
|
||||||
|
});
|
||||||
|
it('should be empty when cleared', () => {
|
||||||
|
cache.set(request, response);
|
||||||
|
expect(cache.get({ ...request })).toBe(response);
|
||||||
|
cache.clear();
|
||||||
|
expect(cache.get(request)).toBeNull();
|
||||||
|
});
|
||||||
|
it('should have value when set', () => {
|
||||||
|
cache.set(request, response);
|
||||||
|
expect(cache.has(request)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should not have value when set expired', () => {
|
||||||
|
cache.set(request, response, sub(new Date(), { hours: 1 }));
|
||||||
|
expect(cache.has(request)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should not have value when get expired', () => {
|
||||||
|
const now = new Date();
|
||||||
|
cache.set(request, response, add(now, { hours: 1 }));
|
||||||
|
expect(cache.has(request)).toBeTruthy();
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(add(now, { hours: 2 }));
|
||||||
|
expect(cache.has(request)).toBeFalsy();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RecordingSegmentsCache', () => {
|
||||||
|
const cache = new RecordingSegmentsCache();
|
||||||
|
const now = new Date();
|
||||||
|
const range: DateRange = {
|
||||||
|
start: now,
|
||||||
|
end: add(now, { hours: 1 }),
|
||||||
|
};
|
||||||
|
const badRange = { start: sub(now, { hours: 1 }), end: now };
|
||||||
|
const createSegment = (date: Date, id: string): RecordingSegment => {
|
||||||
|
return {
|
||||||
|
start_time: date.getTime() / 1000,
|
||||||
|
end_time: date.getTime() / 1000 + 10,
|
||||||
|
id: id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const segments = [
|
||||||
|
createSegment(now, 'segment-1'),
|
||||||
|
createSegment(add(now, { seconds: 10 }), 'segment-2'),
|
||||||
|
createSegment(add(now, { seconds: 20 }), 'segment-3'),
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cache.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get segments when added', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.get('camera-1', range)).toEqual(segments);
|
||||||
|
});
|
||||||
|
it('should get some segments for shorter range', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.get('camera-1', { ...range, end: add(now, { seconds: 5 }) })).toEqual([
|
||||||
|
segments[0],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('should not get for other range', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.get('camera-1', badRange)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have coverage when added', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.hasCoverage('camera-1', range)).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should not have coverage for other camera', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.hasCoverage('camera-2', range)).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should not have coverage for other range', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.hasCoverage('camera-1', badRange)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be empty when cleared', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
cache.clear();
|
||||||
|
expect(cache.get('camera-1', range)).toBeNull();
|
||||||
|
expect(cache.hasCoverage('camera-1', range)).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return internal cache', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
const internalCache = cache.getCache('camera-1');
|
||||||
|
expect(internalCache).toBeTruthy();
|
||||||
|
expect(internalCache?.get(range)).toEqual(segments);
|
||||||
|
});
|
||||||
|
it('should not return internal cache for wrong camera', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
expect(cache.getCache('camera-2')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return cameraIDs', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
cache.add('camera-2', range, segments);
|
||||||
|
expect(sortBy(cache.getCameraIDs())).toEqual(sortBy(['camera-1', 'camera-2']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove expired matches', () => {
|
||||||
|
cache.add('camera-1', range, segments);
|
||||||
|
cache.expireMatches('camera-1', (segment) => segment === segments[0]);
|
||||||
|
expect(sortBy(cache.get('camera-1', range))).toEqual(segments.splice(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -49,20 +49,20 @@ const createEntity = (entity: Partial<Entity>): Entity => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('getEngineForCamera()', () => {
|
describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
||||||
it('config:frigate', async () => {
|
it('should get frigate engine from config', async () => {
|
||||||
const config = createCameraConfig({ engine: 'frigate' });
|
const config = createCameraConfig({ engine: 'frigate' });
|
||||||
expect(await createFactory().getEngineForCamera(createHASS(), config)).toBe(
|
expect(await createFactory().getEngineForCamera(createHASS(), config)).toBe(
|
||||||
Engine.Frigate,
|
Engine.Frigate,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('config:motionEye', async () => {
|
it('should get motionEye engine from config', async () => {
|
||||||
const config = createCameraConfig({ engine: 'motioneye' });
|
const config = createCameraConfig({ engine: 'motioneye' });
|
||||||
expect(await createFactory().getEngineForCamera(createHASS(), config)).toBe(
|
expect(await createFactory().getEngineForCamera(createHASS(), config)).toBe(
|
||||||
Engine.MotionEye,
|
Engine.MotionEye,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('auto:frigate', async () => {
|
it('should get frigate engine from auto config', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
}).getEngineForCamera(createHASS(), config),
|
}).getEngineForCamera(createHASS(), config),
|
||||||
).toBe(Engine.Frigate);
|
).toBe(Engine.Frigate);
|
||||||
});
|
});
|
||||||
it('auto:motioneye', async () => {
|
it('should get motioneye engine from auto config', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
}).getEngineForCamera(createHASS(), config),
|
}).getEngineForCamera(createHASS(), config),
|
||||||
).toBe(Engine.MotionEye);
|
).toBe(Engine.MotionEye);
|
||||||
});
|
});
|
||||||
it('auto:motioneye', async () => {
|
it('should get generic engine from auto config', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
}).getEngineForCamera(createHASS(), config),
|
}).getEngineForCamera(createHASS(), config),
|
||||||
).toBe(Engine.Generic);
|
).toBe(Engine.Generic);
|
||||||
});
|
});
|
||||||
it('config:frigate:camera_name', async () => {
|
it('should get frigate engine from config with camera_name', async () => {
|
||||||
const config = createCameraConfig({
|
const config = createCameraConfig({
|
||||||
frigate: { client_id: 'bar', camera_name: 'foo' },
|
frigate: { client_id: 'bar', camera_name: 'foo' },
|
||||||
});
|
});
|
||||||
@@ -114,7 +114,7 @@ describe('getEngineForCamera()', () => {
|
|||||||
Engine.Frigate,
|
Engine.Frigate,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('config:frigate:throw', async () => {
|
it('should throw error on invalid entity', async () => {
|
||||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||||
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
const entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
||||||
|
|
||||||
@@ -128,18 +128,18 @@ describe('getEngineForCamera()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createEngine()', () => {
|
describe('CameraManagerEngineFactory.createEngine()', () => {
|
||||||
it('generic', async () => {
|
it('should create generic engine', async () => {
|
||||||
expect(await createFactory().createEngine(Engine.Generic)).toBeInstanceOf(
|
expect(await createFactory().createEngine(Engine.Generic)).toBeInstanceOf(
|
||||||
GenericCameraManagerEngine,
|
GenericCameraManagerEngine,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('frigate', async () => {
|
it('should create frigate engine', async () => {
|
||||||
expect(await createFactory().createEngine(Engine.Frigate)).toBeInstanceOf(
|
expect(await createFactory().createEngine(Engine.Frigate)).toBeInstanceOf(
|
||||||
FrigateCameraManagerEngine,
|
FrigateCameraManagerEngine,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('motioneye', async () => {
|
it('should create motioneye engine', async () => {
|
||||||
expect(await createFactory().createEngine(Engine.MotionEye)).toBeInstanceOf(
|
expect(await createFactory().createEngine(Engine.MotionEye)).toBeInstanceOf(
|
||||||
MotionEyeCameraManagerEngine,
|
MotionEyeCameraManagerEngine,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user