From a8c27de3f4434d8a08e953b95b47a4161f9473ed Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Sat, 29 Nov 2025 09:07:09 +0100 Subject: start simple bank example --- pbt/ts/src/property.ts | 119 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 95 insertions(+), 24 deletions(-) (limited to 'pbt/ts/src/property.ts') diff --git a/pbt/ts/src/property.ts b/pbt/ts/src/property.ts index 7714747..7b3ee59 100644 --- a/pbt/ts/src/property.ts +++ b/pbt/ts/src/property.ts @@ -1,30 +1,31 @@ -import Prando from "prando"; +import Prando from 'prando'; type Result = 'OK' | 'Falsified' | 'Exception'; -type ShrinkResult = { counterexample: A, shrinks: number }; +type ShrinkResult = {counterexample: A; shrinks: number}; type TestResult = { - seed: number | null, - result: Result, - counterexample: ShrinkResult | null + seed: number | null; + result: Result; + counterexample: ShrinkResult | null; }; type Predicate = (a: A) => boolean; -export type Gen = (rng: Prando) => ((size: number) => A); +export type Gen = (rng: Prando) => (size: number) => A; type Shrinker = (a: A) => A[]; -type Property = (seed: number, - size: number) => TestResult; +type Property = (seed: number, size: number) => TestResult; const MAX_SUCCESS = 100; -function findMinimalCounterExample(x: A, +function findMinimalCounterExample( + x: A, predicate: Predicate, shrinker: Shrinker, - depth: number = 0): ShrinkResult { + depth: number = 0, +): ShrinkResult { let xs = shrinker(x); let counterexample = x; let shrinks = depth; @@ -37,33 +38,35 @@ function findMinimalCounterExample(x: A, } } } - return { counterexample, shrinks }; + return {counterexample, shrinks}; } - export function generate(gen: Gen): A { let rng = new Prando(Math.random() * 1000); return gen(rng)(100); } export function property( - predicate: Predicate, generator: Gen, - shrinker: Shrinker): Property { - return (seed: number, - size: number) => { + 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 = findMinimalCounterExample(x, predicate, shrinker); - return { seed, result: 'Falsified', tests: i + 1, counterexample }; + 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 }; + return {seed, result: 'OK', tests: i, counterexample: null}; }; } @@ -82,6 +85,65 @@ export function listOf(gen: Gen): Gen { }; } +/// 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 = []; @@ -100,13 +162,22 @@ export function shrinklist(arr: A[]): A[][] { return result; } -export let someInt: Gen = (rng: Prando) => - (size: number) => - rng.nextInt(-size, size); - 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); + }; +} -- cgit v1.3.1