import {Option, getOrElse, isSome, none, some} from 'fp-ts/lib/Option'; import {LazyArg, pipe} from 'fp-ts/lib/function'; 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; export 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; }; }; } /// Select randomly one element from given list export function elements(arr: A[]): Gen { return (rng: Prando) => (_size: number) => { let index = rng.nextInt(0, arr.length - 1); return arr[index]; }; } export function frequency(distribution: [number, Gen][]): Gen { const total: number = distribution.reduce((acc, [freq, _]) => acc + freq, 0); return (rng: Prando) => (size: number) => { const idx = rng.next(0, total); let selectedGen: Option> = none; selectedGen = distribution.reduce((found, [freq, gen]) => { if (isSome(found)) { return found; } else if (idx <= freq) { return some(gen); } else { return none; } }, selectedGen); const generator: Gen = pipe( selectedGen, getOrElse((() => { throw new Error(`invalid frequency distribution ${distribution}`); }) as LazyArg>), ); return generator(rng)(size); }; } /// Select one key-value pair from given map 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]; }; }; } /// Choose number between some bounds. export function choose(min: number, max: number): Gen { return (rng: Prando) => (size: number) => rng.nextInt(min, max); } /// Choose some random integer. export let someInt: Gen = (rng: Prando) => (size: number) => { const bound = Math.pow(2, size); return rng.nextInt(-bound, bound); }; /// Choose some random positive integer. export let positiveInt: Gen = (rng: Prando) => (size: number) => { const bound = Math.pow(2, Math.round(Math.sqrt(size))); return rng.nextInt(0, bound); }; /// Generate a pair of generated values export function pairOf(gen: Gen): Gen<[A, A]> { return (rng: Prando) => (size: number) => [gen(rng)(size), gen(rng)(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 function shrinkPositive(i: number): number[] { if (i == 0) return []; return [i - 1, Math.round(Math.sqrt(i))]; } // shrink pair export function shrinkPair( shrinkA: Shrinker, shrinkB: Shrinker, ): Shrinker<[A, B]> { return ([x, y]) => { const shrinkx = shrinkA(x); const shrinky = shrinkB(y); // assumes x >= y const resulta: [A, B][] = shrinkx.map(e => [e, y]); const resultb: [A, B][] = shrinky.map(e => [x, e]); const resultboth: [A, B][] = shrinkx.map((e, i) => [e, shrinky[i]]); return resulta.concat(resultb, resultboth); }; } 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); }; }