Files
suaveplan/packages/media/audio/audio-core/tests/property.test.ts
2026-05-27 23:47:53 +01:00

138 lines
5.5 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import * as fc from "fast-check";
import { AudioDocument } from "../src/document/document.js";
import { BLOCK_SIZE } from "../src/page/page.js";
import { PlanEngine } from "../src/plan/plan-engine.js";
import { OpRegistry } from "../src/registry/op-registry.js";
function makeDoc(
sourceLength: number,
channels: number,
sampleRate: number
): AudioDocument {
const pageSize = 48000;
const pages = [];
for (let s = 0; s < sourceLength; s += pageSize) {
const len = Math.min(pageSize, sourceLength - s);
pages.push({
startSample: s,
channelData: Array.from(
{ length: channels },
() => new Float32Array(len)
),
});
}
return new AudioDocument({
sampleRate,
channels,
sourceLength,
pages,
edits: [],
});
}
describe("Property: identity edit preserves plan length", () => {
it("passes with random source lengths and sample rates", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(
fc.integer({ min: 1000, max: 500_000 }),
fc.integer({ min: 1, max: 8 }),
fc.integer({ min: 8000, max: 192_000 }),
(sourceLength, channels, sampleRate) => {
const doc = makeDoc(sourceLength, channels, sampleRate);
const registry = OpRegistry.createWithDefaults();
const engine = new PlanEngine(registry);
const before = engine.compile(doc, true);
doc.run({ type: "speed", opts: { rate: 1 } });
const after = engine.compile(doc, true);
expect(after.totalLen).toBe(before.totalLen);
}
)
);
});
});
describe("Property: plan compilation is deterministic", () => {
it("plan compilation is deterministic", { timeout: 30000 }, () => {
fc.assert(
fc.property(
fc.integer({ min: 1000, max: 200_000 }),
fc.integer({ min: 1, max: 4 }),
fc.integer({ min: 8000, max: 96_000 }),
fc.integer({ min: 1, max: 4 }),
(sourceLength, channels, sampleRate, repeatTimes) => {
const doc = makeDoc(sourceLength, channels, sampleRate);
doc.run({ type: "repeat", opts: { times: repeatTimes } });
doc.run({ type: "gain", opts: { gain: 0.5 } });
const registry = OpRegistry.createWithDefaults();
const engine = new PlanEngine(registry);
const first = engine.compile(doc, true);
const second = engine.compile(doc, true);
expect(second).toEqual(first);
}
)
);
});
});
describe("Property: stream yields exact total sample count", () => {
it("stream yields exact total sample count", {
timeout: 30000,
}, async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 1000, max: 200_000 }),
fc.integer({ min: 1, max: 4 }),
fc.integer({ min: 1, max: BLOCK_SIZE }),
async (sourceLength, channels, blockSize) => {
const doc = makeDoc(sourceLength, channels, 48000);
const registry = OpRegistry.createWithDefaults();
const engine = new PlanEngine(registry);
const plan = engine.compile(doc, false);
let totalSamples = 0;
for await (const block of doc.stream({ blockSize })) {
totalSamples += block[0]?.length ?? 0;
}
expect(totalSamples).toBe(plan.totalLen);
}
)
);
});
});
describe("Property: clone preserves structure with new id", () => {
it("clone toJSON equals original toJSON", { timeout: 30000 }, () => {
// The property under test is structural — `clone().toJSON()` equals
// the original's `toJSON()` modulo `id`. The doc's *size* is
// irrelevant to that invariant, so cap inputs small to keep the
// fast-check sweep inside the 30 s budget. The other three
// properties in this file deliberately exercise large source
// lengths because they probe size-dependent code paths (plan
// compilation, streaming); this one does not.
fc.assert(
fc.property(
fc.integer({ min: 1000, max: 10_000 }),
fc.integer({ min: 1, max: 4 }),
fc.integer({ min: 8000, max: 96_000 }),
(sourceLength, channels, sampleRate) => {
const doc = makeDoc(sourceLength, channels, sampleRate);
doc.run({
type: "trim",
opts: { start: 0, end: sourceLength },
});
const cloned = doc.clone();
const { id: originalId, ...originalRest } = doc.toJSON();
const { id: clonedId, ...clonedRest } = cloned.toJSON();
// The clone gets a fresh id but is otherwise an exact
// structural copy — toJSON matches on every other field.
expect(clonedId).not.toBe(originalId);
expect(clonedRest).toEqual(originalRest);
}
),
{ numRuns: 25 }
);
});
});