diff options
| author | Arnaud Bailly <arnaud@pankzsoft.com> | 2025-11-29 09:07:09 +0100 |
|---|---|---|
| committer | Arnaud Bailly <arnaud@pankzsoft.com> | 2025-11-29 09:07:09 +0100 |
| commit | a8c27de3f4434d8a08e953b95b47a4161f9473ed (patch) | |
| tree | 426e6840756dc40881a6bb7384e7f7107e3a2d88 /pbt/ts/src | |
| parent | 97e0dfa41a50d9b7af7d1769e2e712625efb3175 (diff) | |
| download | lambda-nantes-a8c27de3f4434d8a08e953b95b47a4161f9473ed.tar.gz | |
start simple bank example
Diffstat (limited to 'pbt/ts/src')
| -rw-r--r-- | pbt/ts/src/bank.test.ts | 42 | ||||
| -rw-r--r-- | pbt/ts/src/bank.ts | 31 | ||||
| -rw-r--r-- | pbt/ts/src/index.test.ts | 6 | ||||
| -rw-r--r-- | pbt/ts/src/index.ts | 46 | ||||
| -rw-r--r-- | pbt/ts/src/property.ts | 119 |
5 files changed, 201 insertions, 43 deletions
diff --git a/pbt/ts/src/bank.test.ts b/pbt/ts/src/bank.test.ts new file mode 100644 index 0000000..9ec25c0 --- /dev/null +++ b/pbt/ts/src/bank.test.ts @@ -0,0 +1,42 @@ +import {toNullable} from 'fp-ts/lib/Option'; +import {Account, SimpleBank} from './bank'; +import { + Gen, + Property, + choose, + elements, + forall, + keyValueOf, + mapOf, + mapOf1, + property, +} from './property'; + +const sampleAccounts: Account[] = [ + 'FR12345678901234', + 'DE2345678901234', + 'IT34567890123412', +]; + +const genAccount: Gen<Account> = elements(sampleAccounts); + +const genBalance: Gen<number> = choose(0, 1000000); + +const genAccounts: Gen<Map<Account, number>> = mapOf1(genAccount, genBalance); + +describe('Bank Simulator', () => { + test('can retrieve balance given account exists', () => { + const prop = forall(genAccounts, accounts => { + const api = new SimpleBank(accounts); + return property(keyValueOf(accounts), ([accountNr, bal]) => { + const retrieved = toNullable(api.balance(accountNr)); + return retrieved == bal; + }); + }); + + const result = Property.run(prop); + + console.log(result); + expect(result.result).toBe('OK'); + }); +}); diff --git a/pbt/ts/src/bank.ts b/pbt/ts/src/bank.ts new file mode 100644 index 0000000..bee9dfc --- /dev/null +++ b/pbt/ts/src/bank.ts @@ -0,0 +1,31 @@ +import {Option, fromNullable} from 'fp-ts/lib/Option'; + +// keep it simple +export type Account = string; + +export interface Bank { + balance: (account: Account) => Option<number>; + deposit: (account: Account, amt: number) => void; + withdraw: (account: Account, amt: number) => void; +} + +export class SimpleBank implements Bank { + private accounts: Map<Account, number>; + + constructor(accounts: Map<Account, number>) { + // clone the content of the map + this.accounts = new Map(accounts); + } + + balance(account: Account): Option<number> { + return fromNullable(this.accounts.get(account)); + } + + deposit(account: Account, amt: number): void { + throw new Error('Method not implemented.'); + } + + withdraw(account: Account, amt: number): void { + throw new Error('Method not implemented.'); + } +} diff --git a/pbt/ts/src/index.test.ts b/pbt/ts/src/index.test.ts index 58f5119..1ad1cb0 100644 --- a/pbt/ts/src/index.test.ts +++ b/pbt/ts/src/index.test.ts @@ -1,9 +1,9 @@ -import { prop_reverse_with_two_lists } from './index'; -import { Property } from './property'; +import {prop_reverse_with_two_lists} from './index'; +import {Property} from './property'; describe('testing lists reversal', () => { test('reverse of two appended lists is append of each list reversed', () => { let reverse_append_result = Property.run(prop_reverse_with_two_lists); - expect(reverse_append_result).toBe(true); + expect(reverse_append_result.result).toBe('OK'); }); }); diff --git a/pbt/ts/src/index.ts b/pbt/ts/src/index.ts index 25b97aa..7f67d77 100644 --- a/pbt/ts/src/index.ts +++ b/pbt/ts/src/index.ts @@ -1,12 +1,14 @@ -import "dotenv/config"; +import 'dotenv/config'; import Prando from 'prando'; -import { Gen, Property, listOf, property, shrinklist, someInt } from "./property"; +import {Gen, Property, listOf, property, shrinklist, someInt} from './property'; function arrayEquals<A>(a: A[], b: A[]): boolean { - return Array.isArray(a) && + return ( + Array.isArray(a) && Array.isArray(b) && a.length === b.length && - a.every((val, index) => val === b[index]); + a.every((val, index) => val === b[index]) + ); } function reverse<A>(arr: A[]): A[] { @@ -25,14 +27,16 @@ function gen2lists<A>(gen: Gen<A>): Gen<[A[], A[]]> { return (rng: Prando) => { let gl = listOf(gen)(rng); return (size: number) => { - return [gl(size), gl(size)] + return [gl(size), gl(size)]; }; }; } function reverse_with_two_lists<A>(arrs: [A[], A[]]): boolean { - return arrayEquals(reverse(arrs[0].concat(arrs[1])), - reverse(arrs[0]).concat(reverse(arrs[1]))); + return arrayEquals( + reverse(arrs[0].concat(arrs[1])), + reverse(arrs[1]).concat(reverse(arrs[0])), + ); } function shrink2lists<A>([l1, l2]: [A[], A[]]): [A[], A[]][] { @@ -40,22 +44,32 @@ function shrink2lists<A>([l1, l2]: [A[], A[]]): [A[], A[]][] { let s1 = shrinklist(l1); let s2 = shrinklist(l2); for (let i = 0; i < s1.length; i++) { - let j = s2.length - i -1; + let j = s2.length - i - 1; let news2 = j >= 0 ? s2[j] : []; result.push([s1[i], news2]); } return result; } -export let prop_reverse_with_two_lists = - property<[number[], number[]]>(reverse_with_two_lists, gen2lists(someInt), shrink2lists); +export let prop_reverse_with_two_lists = property<[number[], number[]]>( + gen2lists(someInt), + reverse_with_two_lists, + shrink2lists, +); -let prop_reverse_is_self_inverse = - property(reverse_is_self_inverse, listOf(someInt), shrinklist); +let prop_reverse_is_self_inverse = property( + listOf(someInt), + reverse_is_self_inverse, + shrinklist, +); -async function main() { +async function main(): Promise<void> { let reverse_append_result = Property.run(prop_reverse_with_two_lists); let reverse_reverse_result = Property.run(prop_reverse_is_self_inverse); - console.log('reverse_append_result: ' + JSON.stringify(reverse_append_result)); - console.log('reverse_reverse_result: ' + JSON.stringify(reverse_reverse_result)); -}; + console.log( + 'reverse_append_result: ' + JSON.stringify(reverse_append_result), + ); + console.log( + 'reverse_reverse_result: ' + JSON.stringify(reverse_reverse_result), + ); +} 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<A> = { counterexample: A, shrinks: number }; +type ShrinkResult<A> = {counterexample: A; shrinks: number}; type TestResult<A> = { - seed: number | null, - result: Result, - counterexample: ShrinkResult<A> | null + seed: number | null; + result: Result; + counterexample: ShrinkResult<A> | null; }; type Predicate<A> = (a: A) => boolean; -export type Gen<A> = (rng: Prando) => ((size: number) => A); +export type Gen<A> = (rng: Prando) => (size: number) => A; type Shrinker<A> = (a: A) => A[]; -type Property<A> = (seed: number, - size: number) => TestResult<A>; +type Property<A> = (seed: number, size: number) => TestResult<A>; const MAX_SUCCESS = 100; -function findMinimalCounterExample<A>(x: A, +function findMinimalCounterExample<A>( + x: A, predicate: Predicate<A>, shrinker: Shrinker<A>, - depth: number = 0): ShrinkResult<A> { + depth: number = 0, +): ShrinkResult<A> { let xs = shrinker(x); let counterexample = x; let shrinks = depth; @@ -37,33 +38,35 @@ function findMinimalCounterExample<A>(x: A, } } } - return { counterexample, shrinks }; + return {counterexample, shrinks}; } - export function generate<A>(gen: Gen<A>): A { let rng = new Prando(Math.random() * 1000); return gen(rng)(100); } export function property<A>( - predicate: Predicate<A>, generator: Gen<A>, - shrinker: Shrinker<A>): Property<A> { - return (seed: number, - size: number) => { + predicate: Predicate<A>, + shrinker?: Shrinker<A>, +): Property<A> { + 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<A>(gen: Gen<A>): Gen<A[]> { }; } +/// generates a map with exactly size elements +export function mapOf<K, V>(genK: Gen<K>, genV: Gen<V>): Gen<Map<K, V>> { + return (rng: Prando) => { + let gk = genK(rng); + let gv = genV(rng); + return (size: number) => { + let result = new Map<K, V>(); + 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<K, V>(genK: Gen<K>, genV: Gen<V>): Gen<Map<K, V>> { + return (rng: Prando) => { + let gk = genK(rng); + let gv = genV(rng); + return (size: number) => { + let result = new Map<K, V>(); + for (let i = 0; i <= size; i++) { + result.set(gk(size), gv(size)); + } + return result; + }; + }; +} + +export function elements<A>(arr: A[]): Gen<A> { + return (rng: Prando) => (_size: number) => { + let index = rng.nextInt(0, arr.length - 1); + return arr[index]; + }; +} + +export function keyValueOf<K, V>(map: Map<K, V>): 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<number> { + return (rng: Prando) => (size: number) => rng.nextInt(min, max); +} + +export let someInt: Gen<number> = (rng: Prando) => (size: number) => + rng.nextInt(-size, size); + +// basic shrinkers export function shrinklist<A>(arr: A[]): A[][] { let result = []; @@ -100,13 +162,22 @@ export function shrinklist<A>(arr: A[]): A[][] { return result; } -export let someInt: Gen<number> = (rng: Prando) => - (size: number) => - rng.nextInt(-size, size); - export const Property = { run<A>(prop: Property<A>): TestResult<A> { let seed = Math.floor(Math.random() * 1000000); return prop(seed, 0); - } + }, }; + +// helpers + +export function forall<A, B>( + gen: Gen<A>, + k: (a: A) => Property<B>, +): Property<B> { + return (seed: number, size: number) => { + let rng = new Prando(seed); + let a = gen(rng)(size); + return k(a)(seed, size); + }; +} |
