Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

test/property.ts

Raw
import { 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}`,
	);
}