162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
DEFAULT_JOB_TIMEOUT_MILLISECONDS,
|
|
EngineManager,
|
|
MAX_CONTACT_SHEETS_PER_ENGINE,
|
|
MAX_EXECUTIONS_PER_ENGINE,
|
|
MAX_PROBE_TIMEOUT_MILLISECONDS,
|
|
MIN_PROBE_TIMEOUT_MILLISECONDS,
|
|
MULTITHREAD_CODEC_THREAD_LIMIT,
|
|
engineJobTimeoutMilliseconds,
|
|
prepareEngineArguments,
|
|
probeTimeoutMilliseconds,
|
|
shouldRecycleExecutionCore,
|
|
} from '../../src/ffmpeg/EngineManager';
|
|
|
|
describe('prepareEngineArguments', () => {
|
|
it('leaves single-thread commands unchanged', () => {
|
|
const args = ['-i', '/input/source.mp4', '/work/output.mp4'];
|
|
expect(
|
|
prepareEngineArguments(
|
|
args,
|
|
'single-thread',
|
|
['/input/source.mp4'],
|
|
['/work/output.mp4']
|
|
)
|
|
).toEqual(args);
|
|
});
|
|
|
|
it('bounds multithread input and output codec threads', () => {
|
|
expect(
|
|
prepareEngineArguments(
|
|
['-i', '/input/source.mp4', '-c:v', 'libx264', '/work/output.mp4'],
|
|
'multithread',
|
|
['/input/source.mp4'],
|
|
['/work/output.mp4']
|
|
)
|
|
).toEqual([
|
|
'-threads',
|
|
String(MULTITHREAD_CODEC_THREAD_LIMIT),
|
|
'-i',
|
|
'/input/source.mp4',
|
|
'-c:v',
|
|
'libx264',
|
|
'-threads',
|
|
String(MULTITHREAD_CODEC_THREAD_LIMIT),
|
|
'/work/output.mp4',
|
|
]);
|
|
});
|
|
|
|
it('bounds a generated output pattern in the final output position', () => {
|
|
expect(
|
|
prepareEngineArguments(
|
|
['-i', '/input/source.mp4', '-f', 'segment', '/work/part-%03d.mp4'],
|
|
'multithread',
|
|
['/input/source.mp4'],
|
|
['/work/part-001.mp4', '/work/part-002.mp4']
|
|
)
|
|
).toEqual([
|
|
'-threads',
|
|
'2',
|
|
'-i',
|
|
'/input/source.mp4',
|
|
'-f',
|
|
'segment',
|
|
'-threads',
|
|
'2',
|
|
'/work/part-%03d.mp4',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('engineJobTimeoutMilliseconds', () => {
|
|
it('uses a bounded duration-aware watchdog', () => {
|
|
expect(engineJobTimeoutMilliseconds({ expectedDurationSeconds: 2 })).toBe(
|
|
120_000
|
|
);
|
|
expect(
|
|
engineJobTimeoutMilliseconds({ expectedDurationSeconds: 10_000 })
|
|
).toBe(DEFAULT_JOB_TIMEOUT_MILLISECONDS);
|
|
});
|
|
|
|
it('honors a shorter explicit timeout and caps a longer one', () => {
|
|
expect(
|
|
engineJobTimeoutMilliseconds({
|
|
expectedDurationSeconds: 30,
|
|
timeoutMilliseconds: 5_000,
|
|
})
|
|
).toBe(5_000);
|
|
expect(
|
|
engineJobTimeoutMilliseconds({
|
|
timeoutMilliseconds: DEFAULT_JOB_TIMEOUT_MILLISECONDS * 2,
|
|
})
|
|
).toBe(DEFAULT_JOB_TIMEOUT_MILLISECONDS);
|
|
});
|
|
});
|
|
|
|
describe('probeTimeoutMilliseconds', () => {
|
|
it('keeps small probes responsive, scales real media, and caps the watchdog', () => {
|
|
expect(probeTimeoutMilliseconds(34_057)).toBe(
|
|
MIN_PROBE_TIMEOUT_MILLISECONDS
|
|
);
|
|
expect(probeTimeoutMilliseconds(153_019_068)).toBe(72_966);
|
|
expect(probeTimeoutMilliseconds(2 * 1024 * 1024 * 1024)).toBe(
|
|
MAX_PROBE_TIMEOUT_MILLISECONDS
|
|
);
|
|
});
|
|
|
|
it('rejects an already-cancelled probe before loading an engine', async () => {
|
|
const factory = vi.fn(async () => {
|
|
throw new Error('The engine factory must not be called.');
|
|
});
|
|
const manager = new EngineManager(factory);
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
|
|
await expect(
|
|
manager.probe(
|
|
new File(['media'], 'cancelled.mp4', { type: 'video/mp4' }),
|
|
undefined,
|
|
controller.signal
|
|
)
|
|
).rejects.toMatchObject({
|
|
code: 'cancelled',
|
|
message: 'The media probe was cancelled.',
|
|
});
|
|
expect(factory).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('execution core recycling policy', () => {
|
|
it('refreshes only after the bounded idle-core job budget is exhausted', () => {
|
|
expect(shouldRecycleExecutionCore(MAX_EXECUTIONS_PER_ENGINE - 1)).toBe(
|
|
false
|
|
);
|
|
expect(shouldRecycleExecutionCore(MAX_EXECUTIONS_PER_ENGINE)).toBe(true);
|
|
});
|
|
|
|
it('refreshes before a fourth contact sheet hits retained tile-filter state', () => {
|
|
expect(
|
|
shouldRecycleExecutionCore(
|
|
3,
|
|
'contact-sheet',
|
|
MAX_CONTACT_SHEETS_PER_ENGINE - 1
|
|
)
|
|
).toBe(false);
|
|
expect(
|
|
shouldRecycleExecutionCore(
|
|
3,
|
|
'contact-sheet',
|
|
MAX_CONTACT_SHEETS_PER_ENGINE
|
|
)
|
|
).toBe(true);
|
|
expect(
|
|
shouldRecycleExecutionCore(
|
|
3,
|
|
'thumbnail-series',
|
|
MAX_CONTACT_SHEETS_PER_ENGINE
|
|
)
|
|
).toBe(false);
|
|
});
|
|
});
|