Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

test/fuzz.ts

Raw
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { assertProperty, fc } from "./property.ts";

export { fc };

/** Hostile strings: arbitrary UTF-16 units, control chars, long runs. */
export const hostileString: fc.Arbitrary<string> = fc.string({
	unit: "binary",
	maxLength: 8192,
});

export interface FuzzOptions {
	seed?: number;
	numRuns?: number;
	/** Documented error types that are acceptable rejections, not findings. */
	allow?: ReadonlyArray<abstract new (...args: never[]) => Error>;
}

/**
 * Fuzz a parser over hostile string input. Any throw outside `allow` is a
 * finding: fast-check shrinks it to a minimal input and `assertProperty` stores
 * it under `<target dir>/regressions/`, where the paired replay test picks it up.
 */
export function fuzz(
	targetFile: string,
	name: string,
	exercise: (input: string) => void,
	options: FuzzOptions = {},
): void {
	const allow = options.allow ?? [];
	assertProperty(
		targetFile,
		name,
		fc.property(hostileString, (input) => {
			try {
				exercise(input);
				return true;
			} catch (error) {
				return (
					error instanceof Error && allow.some((kind) => error instanceof kind)
				);
			}
		}),
		{
			...(options.seed !== undefined ? { seed: options.seed } : {}),
			numRuns: options.numRuns ?? 2000,
		},
	);
}

/** Minimal inputs stored by earlier fuzz runs beside `targetFile`. */
export function fuzzRegressions(targetFile: string): string[] {
	const dir = join(dirname(fileURLToPath(targetFile)), "regressions");
	try {
		return readdirSync(dir)
			.filter((file) => file.endsWith(".json"))
			.map((file) => {
				const record = JSON.parse(readFileSync(join(dir, file), "utf8")) as {
					counterexample?: unknown[];
				};
				return record.counterexample?.[0];
			})
			.filter((input): input is string => typeof input === "string");
	} catch {
		return [];
	}
}

/** Re-run a stored input; rethrow only errors outside the documented `allow` set. */
export function replay(
	exercise: (input: string) => void,
	allow: ReadonlyArray<abstract new (...args: never[]) => Error>,
	input: string,
): void {
	try {
		exercise(input);
	} catch (error) {
		if (
			!(error instanceof Error && allow.some((kind) => error instanceof kind))
		)
			throw error;
	}
}

/** True when run directly as `node <target>.fuzz.ts`, not when imported. */
export function isMain(importMetaUrl: string): boolean {
	const entry = process.argv[1];
	return entry ? importMetaUrl === `file://${entry}` : false;
}