summaryrefslogtreecommitdiff
path: root/pbt/ts/src
diff options
context:
space:
mode:
Diffstat (limited to 'pbt/ts/src')
-rw-r--r--pbt/ts/src/bank.ts10
-rw-r--r--pbt/ts/src/property.test.ts34
-rw-r--r--pbt/ts/src/property.ts45
3 files changed, 86 insertions, 3 deletions
diff --git a/pbt/ts/src/bank.ts b/pbt/ts/src/bank.ts
index bee9dfc..c5adc05 100644
--- a/pbt/ts/src/bank.ts
+++ b/pbt/ts/src/bank.ts
@@ -5,6 +5,16 @@ export type Account = string;
export interface Bank {
balance: (account: Account) => Option<number>;
+
+ /// Deposit amount into account
+ ///
+ /// This function increases the balance of the given account by the
+ /// specified amount. It's a `void` function hence implicitly
+ /// stateful: It's expected to change some internal state of the
+ /// implementing class.
+ ///
+ /// @param account The account to deposit into
+ /// @param amt The amount to deposit
deposit: (account: Account, amt: number) => void;
withdraw: (account: Account, amt: number) => void;
}
diff --git a/pbt/ts/src/property.test.ts b/pbt/ts/src/property.test.ts
new file mode 100644
index 0000000..c4e089d
--- /dev/null
+++ b/pbt/ts/src/property.test.ts
@@ -0,0 +1,34 @@
+import {
+ pairOf,
+ positiveInt,
+ property,
+ shrinkPair,
+ shrinkPositive,
+ someInt,
+} 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;
+
+describe('Find minimal counterexample', () => {
+ test('do not shrink example if no shrinker given', () => {
+ const result = property(pairOf(positiveInt), predicate1)(42, 1);
+ expect(result.result).toBe('Falsified');
+ expect(result.counterexample).toBeNull;
+ });
+
+ test('shrink example if shrinker given', () => {
+ const result = property(
+ pairOf(positiveInt),
+ predicate1,
+ shrinkPair(shrinkPositive, shrinkPositive),
+ )(42, 1);
+ expect(result.result).toBe('Falsified');
+ if (result.counterexample !== null) {
+ expect(result.counterexample.counterexample).toStrictEqual([10, 10]);
+ } else {
+ fail('no counterexample');
+ }
+ });
+});
diff --git a/pbt/ts/src/property.ts b/pbt/ts/src/property.ts
index 7b3ee59..500c423 100644
--- a/pbt/ts/src/property.ts
+++ b/pbt/ts/src/property.ts
@@ -20,7 +20,7 @@ type Property<A> = (seed: number, size: number) => TestResult<A>;
const MAX_SUCCESS = 100;
-function findMinimalCounterExample<A>(
+export function findMinimalCounterExample<A>(
x: A,
predicate: Predicate<A>,
shrinker: Shrinker<A>,
@@ -117,6 +117,7 @@ export function mapOf1<K, V>(genK: Gen<K>, genV: Gen<V>): Gen<Map<K, V>> {
};
}
+/// Select randomly one element from given list
export function elements<A>(arr: A[]): Gen<A> {
return (rng: Prando) => (_size: number) => {
let index = rng.nextInt(0, arr.length - 1);
@@ -124,6 +125,7 @@ export function elements<A>(arr: A[]): Gen<A> {
};
}
+/// Select one key-value pair from given map
export function keyValueOf<K, V>(map: Map<K, V>): Gen<[K, V]> {
let keys = Array.from(map.keys());
return (rng: Prando) => {
@@ -136,12 +138,27 @@ export function keyValueOf<K, V>(map: Map<K, V>): Gen<[K, V]> {
};
}
+/// Choose number between some bounds.
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);
+/// Choose some random integer.
+export let someInt: Gen<number> = (rng: Prando) => (size: number) => {
+ const bound = Math.pow(2, size);
+ return rng.nextInt(-bound, bound);
+};
+
+/// Choose some random positive integer.
+export let positiveInt: Gen<number> = (rng: Prando) => (size: number) => {
+ const bound = Math.pow(2, Math.round(Math.sqrt(size)));
+ return rng.nextInt(0, bound);
+};
+
+/// Generate a pair of generated values
+export function pairOf<A>(gen: Gen<A>): Gen<[A, A]> {
+ return (rng: Prando) => (size: number) => [gen(rng)(size), gen(rng)(size)];
+}
// basic shrinkers
export function shrinklist<A>(arr: A[]): A[][] {
@@ -162,6 +179,28 @@ export function shrinklist<A>(arr: A[]): A[][] {
return result;
}
+export function shrinkPositive(i: number): number[] {
+ if (i == 0) return [];
+
+ return [i - 1, Math.round(Math.sqrt(i))];
+}
+
+// shrink pair
+export function shrinkPair<A, B>(
+ shrinkA: Shrinker<A>,
+ shrinkB: Shrinker<B>,
+): Shrinker<[A, B]> {
+ return ([x, y]) => {
+ const shrinkx = shrinkA(x);
+ const shrinky = shrinkB(y);
+ // assumes x >= y
+ const resulta: [A, B][] = shrinkx.map(e => [e, y]);
+ const resultb: [A, B][] = shrinky.map(e => [x, e]);
+ const resultboth: [A, B][] = shrinkx.map((e, i) => [e, shrinky[i]]);
+ return resulta.concat(resultb, resultboth);
+ };
+}
+
export const Property = {
run<A>(prop: Property<A>): TestResult<A> {
let seed = Math.floor(Math.random() * 1000000);