Files
av-tools/tests/unit/ffmpeg-engine-arguments.test.ts

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);
});
});