import {toNullable} from 'fp-ts/lib/Option'; import {Account, Operation, SimpleBank} from './bank'; import { Gen, Property, choose, elements, forall, frequency, keyValueOf, listOf, mapOf1 as nonEmptyMap, property, } from './property'; import Prando from 'prando'; import {LevelBank} from './levelbank'; import {withTempFile} from './fs'; const sampleAccounts: Account[] = [ 'FR12345678901234', 'DE2345678901234', 'IT34567890123412', ]; const genAccount: Gen = elements(sampleAccounts); const genBalance: Gen = choose(0, 1000000); const genAccounts: Gen> = nonEmptyMap( genAccount, genBalance, ); const genGetBalance: (accounts: Map) => Gen = (accounts: Map) => (rng: Prando) => (size: number) => { const account = elements(Array.from(accounts.keys()))(rng)(size); return {op: 'GetBalance', account}; }; const genDeposit: (accounts: Map) => Gen = (accounts: Map) => (rng: Prando) => (size: number) => { const account = elements(Array.from(accounts.keys()))(rng)(size); const gAmt = choose(1, 100000)(rng)(size); return {op: 'Deposit', account, amount: gAmt}; }; const genWithdraw: (accounts: Map) => Gen = (accounts: Map) => (rng: Prando) => (size: number) => { const account = elements(Array.from(accounts.keys()))(rng)(size); const gAmt = choose(1, 100000)(rng)(size); return {op: 'Withdraw', account, amount: gAmt}; }; const genOneOperation = (accounts: Map) => frequency([ [1, genGetBalance(accounts)], [3, genDeposit(accounts)], [3, genWithdraw(accounts)], ]); const genOperations = (accounts: Map) => listOf(genOneOperation(accounts)); const correctBalance = (initialAccounts: Map) => async (ops: Operation[]): Promise => { const fakeBank = new SimpleBank(initialAccounts); const realBank = await withTempFile(filename => new LevelBank(filename)) .then(bank => bank.populateWith(initialAccounts)) .then(bank => bank); for (const op of ops) { switch (op.op) { case 'GetBalance': { const retrieved = await realBank.balance(op.account).then(toNullable); const expected = await fakeBank.balance(op.account).then(toNullable); if (retrieved !== expected) { console.log( `Balance mismatch for account ${op.account}: expected ${expected}, got ${retrieved}`, ); return false; } break; } case 'Deposit': { await realBank.deposit(op.account, op.amount); await fakeBank.deposit(op.account, op.amount); break; } case 'Withdraw': { await realBank.withdraw(op.account, op.amount); await fakeBank.withdraw(op.account, op.amount); break; } } } // finally check all balances for (const account of initialAccounts.keys()) { const actual = await realBank.balance(account).then(toNullable); const expected = await fakeBank.balance(account).then(toNullable); if (actual !== expected) { console.log( `Final balance mismatch for account ${account}: expected ${expected}, got ${actual}`, ); return false; } } return true; }; describe('Bank Simulator', () => { test('can retrieve balance given account exists', async () => { const prop = forall(genAccounts, async accounts => { const api = new SimpleBank(accounts); return property(keyValueOf(accounts), async ([accountNr, bal]) => { const retrieved = toNullable(await api.balance(accountNr)); return Promise.resolve(retrieved === bal); }); }); const result = await Property.run(prop); expect(result.result).toBe('OK'); }); 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 = await Property.run(prop); console.log(result); console.log(JSON.stringify(result.counterexample)); expect(result.result).toBe('OK'); }); });