summaryrefslogtreecommitdiff
path: root/pbt
diff options
context:
space:
mode:
Diffstat (limited to 'pbt')
-rw-r--r--pbt/stateful-pbt/stateful-pbt.cabal3
-rw-r--r--pbt/ts/jest.config.js2
-rw-r--r--pbt/ts/package-lock.json8
-rw-r--r--pbt/ts/package.json1
-rw-r--r--pbt/ts/src/bank.test.ts42
-rw-r--r--pbt/ts/src/bank.ts31
-rw-r--r--pbt/ts/src/index.test.ts6
-rw-r--r--pbt/ts/src/index.ts46
-rw-r--r--pbt/ts/src/property.ts119
9 files changed, 214 insertions, 44 deletions
diff --git a/pbt/stateful-pbt/stateful-pbt.cabal b/pbt/stateful-pbt/stateful-pbt.cabal
index 6af9056..36005c6 100644
--- a/pbt/stateful-pbt/stateful-pbt.cabal
+++ b/pbt/stateful-pbt/stateful-pbt.cabal
@@ -48,6 +48,7 @@ test-suite minilang-test
main-is: Spec.hs
other-modules:
Exalt.APISpec
+ Exalt.Hyper
hs-source-dirs:
test
default-extensions:
@@ -74,4 +75,6 @@ test-suite minilang-test
, filepath
, haskeline
, hspec
+ , mtl
+ , semilattices
default-language: Haskell2010
diff --git a/pbt/ts/jest.config.js b/pbt/ts/jest.config.js
index 99bd920..5528a39 100644
--- a/pbt/ts/jest.config.js
+++ b/pbt/ts/jest.config.js
@@ -2,5 +2,5 @@ module.exports = {
transform: {'^.+\\.ts?$': 'ts-jest'},
testEnvironment: 'node',
testRegex: '.*\\.(test|spec)?\\.(ts|tsx)$',
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};
diff --git a/pbt/ts/package-lock.json b/pbt/ts/package-lock.json
index d45b356..4396ee6 100644
--- a/pbt/ts/package-lock.json
+++ b/pbt/ts/package-lock.json
@@ -6,6 +6,7 @@
"": {
"dependencies": {
"dotenv": "^16.4.7",
+ "fp-ts": "^2.16.11",
"prando": "^6.0.1",
"typescript": "^5.7.3"
},
@@ -49,6 +50,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -3524,6 +3526,12 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fp-ts": {
+ "version": "2.16.11",
+ "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.11.tgz",
+ "integrity": "sha512-LaI+KaX2NFkfn1ZGHoKCmcfv7yrZsC3b8NtWsTVQeHkq4F27vI5igUuO53sxqDEa2gNQMHFPmpojDw/1zmUK7w==",
+ "license": "MIT"
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
diff --git a/pbt/ts/package.json b/pbt/ts/package.json
index ff17a45..10cb375 100644
--- a/pbt/ts/package.json
+++ b/pbt/ts/package.json
@@ -1,6 +1,7 @@
{
"dependencies": {
"dotenv": "^16.4.7",
+ "fp-ts": "^2.16.11",
"prando": "^6.0.1",
"typescript": "^5.7.3"
},
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);
+ };
+}