From 8a6cfd0187b149a1ce83f62af62bb227ee441277 Mon Sep 17 00:00:00 2001 From: Arnaud Bailly Date: Tue, 2 Dec 2025 04:24:15 +0100 Subject: Make property async aware --- pbt/ts/src/bank.test.ts | 18 +++++++++--------- pbt/ts/src/index.test.ts | 6 ++++-- pbt/ts/src/index.ts | 14 ++++++++------ pbt/ts/src/property.test.ts | 12 ++++++------ pbt/ts/src/property.ts | 24 ++++++++++++++---------- 5 files changed, 41 insertions(+), 33 deletions(-) (limited to 'pbt/ts') diff --git a/pbt/ts/src/bank.test.ts b/pbt/ts/src/bank.test.ts index d503faf..f6aa7b0 100644 --- a/pbt/ts/src/bank.test.ts +++ b/pbt/ts/src/bank.test.ts @@ -61,7 +61,7 @@ const genOperations = (accounts: Map) => const correctBalance = (initialAccounts: Map) => - (ops: Operation[]): boolean => { + (ops: Operation[]): Promise => { const expectedBalances = new Map(initialAccounts); const api = new SimpleBank(initialAccounts); @@ -74,7 +74,7 @@ const correctBalance = console.log( `Balance mismatch for account ${op.account}: expected ${expected}, got ${retrieved}`, ); - return false; + return Promise.resolve(false); } break; } @@ -100,34 +100,34 @@ const correctBalance = console.log( `Final balance mismatch for account ${account}: expected ${expectedBal}, got ${retrieved}`, ); - return false; + return Promise.resolve(false); } } - return true; + return Promise.resolve(true); }; describe('Bank Simulator', () => { - test('can retrieve balance given account exists', () => { + test('can retrieve balance given account exists', async () => { 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; + return Promise.resolve(retrieved === bal); }); }); - const result = Property.run(prop); + const result = await Property.run(prop); expect(result.result).toBe('OK'); }); - test('correctly update balance given a sequence of deposits and withdrawals', () => { + test('correctly update balance given a sequence of deposits and withdrawals', async () => { const prop = forall(genAccounts, accounts => { return property(genOperations(accounts), correctBalance(accounts)); }); - const result = Property.run(prop); + const result = await Property.run(prop); expect(result.result).toBe('OK'); }); diff --git a/pbt/ts/src/index.test.ts b/pbt/ts/src/index.test.ts index b0d5cdc..b102fe3 100644 --- a/pbt/ts/src/index.test.ts +++ b/pbt/ts/src/index.test.ts @@ -2,8 +2,10 @@ 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', () => { - const reverse_append_result = Property.run(prop_reverse_with_two_lists); + test('reverse of two appended lists is append of each list reversed', async () => { + const reverse_append_result = await Property.run( + prop_reverse_with_two_lists, + ); expect(reverse_append_result.result).toBe('OK'); }); }); diff --git a/pbt/ts/src/index.ts b/pbt/ts/src/index.ts index 38049b2..2f59f0e 100644 --- a/pbt/ts/src/index.ts +++ b/pbt/ts/src/index.ts @@ -19,8 +19,8 @@ function reverse(arr: A[]): A[] { return result; } -function reverse_is_self_inverse(arr: number[]): boolean { - return arrayEquals(reverse(reverse(arr)), arr); +function reverse_is_self_inverse(arr: number[]): Promise { + return Promise.resolve(arrayEquals(reverse(reverse(arr)), arr)); } function gen2lists(gen: Gen): Gen<[A[], A[]]> { @@ -32,10 +32,12 @@ function gen2lists(gen: Gen): Gen<[A[], A[]]> { }; } -function reverse_with_two_lists(arrs: [A[], A[]]): boolean { - return arrayEquals( - reverse(arrs[0].concat(arrs[1])), - reverse(arrs[1]).concat(reverse(arrs[0])), +function reverse_with_two_lists(arrs: [A[], A[]]): Promise { + return Promise.resolve( + arrayEquals( + reverse(arrs[0].concat(arrs[1])), + reverse(arrs[1]).concat(reverse(arrs[0])), + ), ); } diff --git a/pbt/ts/src/property.test.ts b/pbt/ts/src/property.test.ts index 920457a..addff34 100644 --- a/pbt/ts/src/property.test.ts +++ b/pbt/ts/src/property.test.ts @@ -7,18 +7,18 @@ import { } from './property'; // from https://github.com/jlink/shrinking-challenge/blob/main/challenges/difference.md -const predicate1: (pair: [number, number]) => boolean = ([x, y]) => - x < 10 || x !== y; +const predicate1: (pair: [number, number]) => Promise = ([x, y]) => + Promise.resolve(x < 10 || x !== y); describe('Find minimal counterexample', () => { - test('do not shrink example if no shrinker given', () => { - const result = property(pairOf(positiveInt), predicate1)(42, 1); + test('do not shrink example if no shrinker given', async () => { + const result = await property(pairOf(positiveInt), predicate1)(42, 1); expect(result.result).toBe('Falsified'); expect(result.counterexample).toBeNull; }); - test('shrink example if shrinker given', () => { - const result = property( + test('shrink example if shrinker given', async () => { + const result = await property( pairOf(positiveInt), predicate1, shrinkPair(shrinkPositive, shrinkPositive), diff --git a/pbt/ts/src/property.ts b/pbt/ts/src/property.ts index 2b69109..56683c1 100644 --- a/pbt/ts/src/property.ts +++ b/pbt/ts/src/property.ts @@ -12,28 +12,28 @@ type TestResult = { counterexample: ShrinkResult | null; }; -type Predicate = (a: A) => boolean; +type Predicate = (a: A) => Promise; 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) => Promise>; const MAX_SUCCESS = 100; -export function findMinimalCounterExample( +export async function findMinimalCounterExample( x: A, predicate: Predicate, shrinker: Shrinker, depth = 0, -): ShrinkResult { +): Promise> { const xs = shrinker(x); let counterexample = x; let shrinks = depth; for (const y of xs) { - if (!predicate(y)) { - const shrink = findMinimalCounterExample( + if (!(await predicate(y))) { + const shrink = await findMinimalCounterExample( y, predicate, shrinker, @@ -58,16 +58,20 @@ export function property( predicate: Predicate, shrinker?: Shrinker, ): Property { - return (seed: number, size: number) => { + return async (seed: number, size: number) => { const rng = new Prando(seed); const gen = generator(rng); let i = 0; for (; i < MAX_SUCCESS; i++) { const x = gen(size); - if (!predicate(x)) { + if (!(await predicate(x))) { let counterexample = {counterexample: x, shrinks: 0}; if (shrinker !== undefined) { - counterexample = findMinimalCounterExample(x, predicate, shrinker); + counterexample = await findMinimalCounterExample( + x, + predicate, + shrinker, + ); } return {seed, result: 'Falsified', tests: i + 1, counterexample}; } @@ -239,7 +243,7 @@ export function shrinkPair( } export const Property = { - run(prop: Property): TestResult { + run(prop: Property): Promise> { const seed = Math.floor(Math.random() * 1000000); return prop(seed, 0); }, -- cgit v1.3.1