Testing

Running the test suite, the four test groups, adding a new test, and debugging common decoder-open failures.

Running the suite

Build first, then run:

source ~/emsdk/emsdk_env.sh
PRESET=standard TARGET=cpu bash scripts/build.sh
node tests/test.mjs

A clean build ends with a summary line:

231 tests — 229 passed, 0 failed, 2 skipped

The count grows as tests are added; what matters is 0 failed. The skip is the WebGPU bench test, which only runs when dist/webgpu.js is present.

Test layout

tests/test.mjs is a single file with no external framework. It has four test groups, each exercising a different layer of the stack:

testBuild — sanity checks that the WASM module loaded correctly: pipeline_version() returns a non-zero integer and pipeline_run_rgba is exported.

testPipelineEdgeCases — calls pipeline_run_rgba directly with various filtergraph strings: an invalid graph returns a negative error code, a same-size passthrough produces correct output, and a chained scale,format graph works end to end.

testDecoderApi — exercises the raw C decoder API. Covers garbage input rejection, invalid-handle guards, opening a PNG via decoder_open_file, reading frame dimensions and fps, decoding a frame and checking RGBA output, post-close guards, and two concurrent decoder slots.

testFFmpegClass — exercises the high-level FFmpeg class: load, double-load idempotency, on()/off() events, createDir/listDir, writeFile/readFile round-trip, the exec() not-available error, and terminate().

testGpu — exercises the gpu namespace: load, scale, explicit filtergraph, benchCpu, benchGpu (returns -1 on CPU build), createDecoder error path, and createDecoderFile success path.

Adding a test

Find the appropriate group and use the ok(msg, condition) helper. All test functions are async.

// Inside testDecoderApi():
{
    const bytes = makeTinyPng(16, 16);
    mod.FS.writeFile('/test.png', new Uint8Array(bytes));
    const h = mod.ccall('decoder_open_file', 'number', ['string'], ['/test.png']);
    ok('open 16×16 PNG', h >= 0);
    ok('width is 16', mod.ccall('decoder_width', 'number', ['number'], [h]) === 16);
    mod.ccall('decoder_close', null, ['number'], [h]);
}

If a test only makes sense under a specific condition, use skip():

if (!someCondition) {
    skip('reason this cannot run here');
} else {
    ok('the assertion', result === expected);
}

When adding a new C export, add it to DECODER_EXPORTS or CPU_EXPORTS in scripts/build.sh and relink before testing. See the C ABI reference for ccall type mappings.

FATE reports

Two reports run a gpl-cpu build against FFmpeg’s FATE sample suite. They don’t gate commits — they track where the library stands over time.

Coverage (make compat → COMPAT.md) decodes each FATE sample and records whether it ran without erroring, bucketed by codec.

Correctness (make fate → CORRECTNESS.md) is stricter. For each pure-decode video framecrc test it decodes every frame via decoder_next_raw_frame (native pixel format), takes the per-frame Adler-32, and compares it to FFmpeg’s vendored reference in vendor/ffmpeg/tests/ref/fate/. A test passes only when every frame is byte-identical. No native ffmpeg is needed — the reference files match the vendored FFmpeg version, so this is FFmpeg’s own pass/fail.

Both accept --filter=<codec> to run one bucket and --no-save to skip writing the report. Each run appends a row to tests/results/, so progress is visible in git history.

What isn’t tested

Real video and audio — H.264, HEVC, VP9, AAC, Opus. Generating valid compressed test vectors in pure JS isn’t practical, and the ffmpeg CLI isn’t compiled into the WASM binary. PNG covers the decoder pipeline because it can be constructed from scratch in pure JS. Real video tests would require shipping binary fixture files.

WebGPU scale — requires a live GPU context, which Node.js doesn’t provide. Test it manually with tests/bench.html (CPU vs WebGPU scale benchmark) and tests/browser-test.html (the gpu / FFmpeg / wasmpeg API surface), served over HTTP so the WASM and WebGPU load.

Encoder over real video — encode() and the C encoder (encoder_open / encoder_push_rgba / encoder_finish) are covered for single-frame image output in test.mjs, but multi-frame video/audio muxing isn’t exercised in the suite yet.

Debugging failures

Silent exit — the WASM module failed to load. Verify dist/cpu.js and dist/cpu.wasm both exist.

decoder_open_file failed: -1330794744 — AVERROR_PROTOCOL_NOT_FOUND. The file protocol wasn’t compiled in. Check:

grep CONFIG_FILE_PROTOCOL vendor/ffmpeg/config_components.h

Should be 1. If it’s 0, add file to the protocols list in configure.mjs and rebuild.

decoder_open_file failed: -1094995529 — AVERROR_INVALIDDATA. Either the input is malformed, or zlib is missing and PNG IDAT decompression is silently failing. Check:

grep CONFIG_ZLIB vendor/ffmpeg/config_components.h

Should be 1. If it’s 0, see Configuration.

decoder_open_file failed: -2 — AVERROR(ENOENT). The path doesn’t exist in the WASM virtual filesystem. Make sure mod.FS.writeFile(path, data) ran before the open call.

“Video: png, none: unspecified size” — avformat_find_stream_info opened the stream but couldn’t determine frame dimensions. Almost always CONFIG_ZLIB 0 — see above.