52 lines
1.7 KiB
TypeScript
52 lines
1.7 KiB
TypeScript
export interface MutationCase<T> {
|
|
name: string;
|
|
value: T;
|
|
}
|
|
|
|
export interface MutationReport<O> {
|
|
baseline: O;
|
|
killed: string[];
|
|
survived: string[];
|
|
}
|
|
|
|
export interface DetectMutationsOptions<T, O> {
|
|
baseline: T;
|
|
mutations: Array<MutationCase<T>>;
|
|
exercise(value: T): O | Promise<O>;
|
|
equivalent?(left: O, right: O): boolean;
|
|
}
|
|
|
|
/**
|
|
* Run the same behavioural probe against a baseline and explicit mutations.
|
|
* A mutation survives when the observable result is unchanged. The helper
|
|
* throws with every survivor by default, making it suitable for an ordinary
|
|
* Bun test and CI without a second runner or source-rewriting process.
|
|
*/
|
|
export async function detectMutations<T, O>(
|
|
options: DetectMutationsOptions<T, O>,
|
|
): Promise<MutationReport<O>> {
|
|
if (!options.mutations.length) {
|
|
throw new Error("WRN-MUTATION-EMPTY: declare at least one meaningful mutation");
|
|
}
|
|
const equivalent = options.equivalent ?? ((left, right) => Object.is(left, right));
|
|
const baseline = await options.exercise(options.baseline);
|
|
const killed: string[] = [];
|
|
const survived: string[] = [];
|
|
for (const mutation of options.mutations) {
|
|
if (!mutation.name.trim()) throw new Error("WRN-MUTATION-NAME: every mutation needs a name");
|
|
let same = false;
|
|
try {
|
|
same = equivalent(baseline, await options.exercise(mutation.value));
|
|
} catch {
|
|
// A behavioural failure is a killed mutation.
|
|
}
|
|
(same ? survived : killed).push(mutation.name);
|
|
}
|
|
if (survived.length) {
|
|
throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), {
|
|
report: { baseline, killed, survived },
|
|
});
|
|
}
|
|
return { baseline, killed, survived };
|
|
}
|