Merge pull request #1060 from dermotduffy/cache-test

Add tests for camera manager cache
This commit is contained in:
Dermot Duffy
2023-04-05 18:31:08 -07:00
committed by GitHub
5 changed files with 164 additions and 20 deletions
+2 -1
View File
@@ -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 ."
} }
+13 -5
View File
@@ -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,7 +107,7 @@ class MemoryRangedCache<Data> {
return output; return output;
} }
public size(): number { public getSize(): number {
return this._data.length; return this._data.length;
} }
@@ -114,7 +118,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 +138,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);
} }
@@ -142,8 +150,8 @@ export class RecordingSegmentsCache {
return this._segments.get(cameraID)?.get(range) ?? null; return this._segments.get(cameraID)?.get(range) ?? null;
} }
public getCache(cameraID: string): MemoryRangedCache<RecordingSegment> | null { public getSize(cameraID: string): number | null {
return this._segments.get(cameraID) ?? null; return this._segments.get(cameraID)?.getSize() ?? null;
} }
public getCameraIDs(): string[] { public getCameraIDs(): string[] {
+2 -2
View File
@@ -990,7 +990,7 @@ export class FrigateCameraManagerEngine
const countSegments = () => const countSegments = () =>
sum( sum(
cameraIDs.map( cameraIDs.map(
(cameraID) => this._recordingSegmentsCache.getCache(cameraID)?.size() ?? 0, (cameraID) => this._recordingSegmentsCache.getSize(cameraID) ?? 0,
), ),
); );
const segmentsStart = countSegments(); const segmentsStart = countSegments();
@@ -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);
}, },
); );
} }
+135
View File
@@ -0,0 +1,135 @@
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 get size', () => {
cache.add('camera-1', range, segments);
expect(cache.getSize("camera-1")).toBe(3);
});
it('should not size for other camera', () => {
cache.add('camera-1', range, segments);
expect(cache.getSize("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));
});
});
+12 -12
View File
@@ -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,
); );