repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
test/property.ts
Rawimport { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import fc, { type RunDetails } from "fast-check";
export { fc };
export interface PropertyOptions {
/** Deterministic seed; recorded so a failure replays exactly. Default `0x5eed`. */
seed?: number;
/** Cases to generate. Default 200. */
numRuns?: number;
}
function slugify(name: string): string {
return (
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") || "property"
);
}
/**
* Run a seeded property. On failure, store the minimal (shrunk) counterexample
* under `<test dir>/regressions/<slug>.json` and throw with the replay seed.
*
* Pass `import.meta.url` as `testFile` so regressions land beside the test.
*/
export function assertProperty<Ts>(
testFile: string,
name: string,
property: fc.IRawProperty<Ts>,
options: PropertyOptions = {},
): void {
const seed = options.seed ?? 0x5eed;
const numRuns = options.numRuns ?? 200;
// Properties are synchronous; narrow the sync|async return of `check`.
const run = fc.check(property, { seed, numRuns }) as RunDetails<Ts>;
if (!run.failed) return;
const dir = join(dirname(fileURLToPath(testFile)), "regressions");
mkdirSync(dir, { recursive: true });
const file = join(dir, `${slugify(name)}.json`);
writeFileSync(
file,
`${JSON.stringify(
{
name,
seed: run.seed,
numRuns: run.numRuns,
counterexamplePath: run.counterexamplePath,
counterexample: run.counterexample,
},
null,
"\t",
)}\n`,
);
throw new Error(
`property "${name}" failed after ${run.numRuns} run(s); replay with seed=${run.seed} path=${run.counterexamplePath}; counterexample=${JSON.stringify(run.counterexample)}; minimal case stored at ${file}`,
);
}