import Prando from 'prando'; type Result = 'OK' | 'Falsified' | 'Exception'; type ShrinkResult = {counterexample: A; shrinks: number}; type TestResult = { seed: number | null; result: Result; counterexample: ShrinkResult | null; }; type Predicate = (a: A) => boolean; export type Gen = (rng: Prando) => (size: number) => A; type Shrinker = (a: A) => A[]; type Property = (seed: number, size: number) => TestResult; const MAX_SUCCESS = 100; function findMinimalCounterExample( x: A, predicate: Predicate, shrinker: Shrinker, depth: number = 0, ): ShrinkResult { let xs = shrinker(x); let counterexample = x; let shrinks = depth; for (let y of xs) { if (!predicate(y)) { let shrink = findMinimalCounterExample(y, predicate, shrinker, depth + 1); if (shrink.shrinks > depth) { counterexample = shrink.counterexample; shrinks = shrink.shrinks; } } } return {counterexample, shrinks}; } export function generate(gen: Gen): A { let rng = new Prando(Math.random() * 1000); return gen(rng)(100); } export function property( generator: Gen, predicate: Predicate, shrinker?: Shrinker, ): Property { return (seed: number, size: number) => { let rng = new Prando(seed); let gen = generator(rng); let i = 0; for (; i < MAX_SUCCESS; i++) { let x = gen(size); if (!predicate(x)) { let counterexample = {counterexample: x, shrinks: 0}; if (shrinker !== undefined) { counterexample = findMinimalCounterExample(x, predicate, shrinker); } return {seed, result: 'Falsified', tests: i + 1, counterexample}; } size++; } return {seed, result: 'OK', tests: i, counterexample: null}; }; } // basic generators export function listOf(gen: Gen): Gen { return (rng: Prando) => { let g = gen(rng); return (size: number) => { let result = []; for (let i = 0; i < size; i++) { result.push(g(size)); } return result; }; }; } /// generates a map with exactly size elements export function mapOf(genK: Gen, genV: Gen): Gen> { return (rng: Prando) => { let gk = genK(rng); let gv = genV(rng); return (size: number) => { let result = new Map(); for (let i = 0; i < size; i++) { result.set(gk(size), gv(size)); } return result; }; }; } /// generates a map with at least size+1 elements. As size is expected /// to be positive or null this guarantees at least one element hence /// the name export function mapOf1(genK: Gen, genV: Gen): Gen> { return (rng: Prando) => { let gk = genK(rng); let gv = genV(rng); return (size: number) => { let result = new Map(); for (let i = 0; i <= size; i++) { result.set(gk(size), gv(size)); } return result; }; }; } export function elements(arr: A[]): Gen { return (rng: Prando) => (_size: number) => { let index = rng.nextInt(0, arr.length - 1); return arr[index]; }; } export function keyValueOf(map: Map): Gen<[K, V]> { let keys = Array.from(map.keys()); return (rng: Prando) => { let gkey = elements(keys)(rng); return (size: number) => { let key = gkey(size); let value = map.get(key)!; return [key, value]; }; }; } export function choose(min: number, max: number): Gen { return (rng: Prando) => (size: number) => rng.nextInt(min, max); } export let someInt: Gen = (rng: Prando) => (size: number) => rng.nextInt(-size, size); // basic shrinkers export function shrinklist(arr: A[]): A[][] { let result = []; let tail = arr.slice(1); let keep1 = arr.slice(); keep1.splice(1, 1); result.push(tail, keep1); if (arr.length > 3) { let half = Math.floor(arr.length / 2); let copy = arr.slice(); let secondHalf = copy.slice(half); result.push(secondHalf); } return result; } export const Property = { run(prop: Property): TestResult { let seed = Math.floor(Math.random() * 1000000); return prop(seed, 0); }, }; // helpers export function forall( gen: Gen, k: (a: A) => Property, ): Property { return (seed: number, size: number) => { let rng = new Prando(seed); let a = gen(rng)(size); return k(a)(seed, size); }; }