Skip to content

Commit

Permalink
Merge b726a55 into 2fd8aae
Browse files Browse the repository at this point in the history
  • Loading branch information
dubzzz committed Nov 3, 2018
2 parents 2fd8aae + b726a55 commit 7ffc3e6
Show file tree
Hide file tree
Showing 18 changed files with 503 additions and 14 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -95,6 +95,7 @@ fast-check has initially been designed in an attempt to cope with limitations I
- replay directly on the minimal counterexample - *no need to replay the whole sequence, you get directly the counterexample*
- custom examples in addition of generated ones - *no need to duplicate the code to play the property on custom examples*
- model based approach - *use the power of property based testing to test UI, APIs or state machines*
- logger per predicate run - *simplify your troubleshoot with fc.context and its logging feature*

For more details, refer to the documentation in the links above.

Expand Down
22 changes: 22 additions & 0 deletions documentation/AdvancedArbitraries.md
Expand Up @@ -130,3 +130,25 @@ Some frameworks actually use this approach. Unfortunately using this approach ma
Let's imagine you are using a `oneof(integer(0, 10), integer(20, 30))` relying on the `DummyArbitrary<number>` above. As soon as you have generated a value - a `number` - you cannot call shrink anymore as you do not know if it has been produced by `integer(0, 10)` or `integer(20, 30)` - in this precise case you can easily infer the producer.

For this reason, the `shrink` method is not part of `Arbitrary` in fast-check but is part of the values instantiated by `generate`.

### Cloneable

Any generated value having a key for `fc.cloneMethod` would be handled a bit differently during the execution. Indeed those values explicitly requires to be cloned before being transmitted again to the predicate.

Cloneable values can be seen as stateful values that would be altered as soon as we use them inside the predicate. For thsi precise reason they have to be recreated if they need to be used inside other runs of the predicate.

Example of usages:
- `fc.context`: is a stateful instance that gathers all the logs for a given predicate execution. In order to provide only the logs linked to the run itself it has to be cloned between all the runs
- stream structure

Example of a stream arbitrary:

```typescript
const streamInt = fc.nat()
.map(seed => {
return Object.assign(
new SeededRandomStream(seed),
{ [fc.cloneMethod]: () => new SeededRandomStream(seed) }
);
});
```
4 changes: 4 additions & 0 deletions documentation/Arbitraries.md
Expand Up @@ -101,6 +101,10 @@ Default for `values` are: `fc.boolean()`, `fc.integer()`, `fc.double()`, `fc.str
- `compareFunc()` generate a comparison function taking two parameters `a` and `b` and producing an integer value. Output is zero when `a` and `b` are considered to be equivalent. Output is strictly inferior to zero means that `a` should be considered strictly inferior to `b` (similar for strictly superior to zero)
- `func(arb: Arbitrary<TOut>)` generate a function of type `(...args: TArgs) => TOut` outputing values generated using `arb`

## Extended tools

- `context()` generate a `Context` instance for each predicate run. `Context` can be used to log stuff within the run itself. In case of failure, the logs will be attached in the counterexample and visible in the stack trace

## Model based testing

Model based testing approach extends the power of property based testing to state machines - *eg.: UI, data-structures*.
Expand Down
23 changes: 23 additions & 0 deletions documentation/Tips.md
Expand Up @@ -164,6 +164,29 @@ Encountered failures were:

With that output, we notice that our `contains` implementation seems to fail when the `pattern` we are looking for is the beginning of the string we are looking in.

## Log within a predicate

In order to ease the diagnosis of red properties, fast-check introduced an internal logger that can be used to log stuff inside the predicate itself.

The advantage of this logger is that one logger is linked to one run so that the counterexample comes with its own logs (and not the ones of previous failures leading to this counterexample). Logs will only be shown in case of failure contrary to `console.log` that would pop everywhere.

Usage is quite simple, logger is one of the features available inside the `Context` interface:

```typescript
fc.assert(
fc.property(
fc.string(),
fc.string(),
fc.context(), // comes with a log method
(a: number, b: number, ctx: fc.Context): boolean => {
const intermediateResult = /* ... */;
ctx.log(`Intermediate: ${intermediateResult}`);
return check(intermediateResult);
}
)
)
```

## Preview generated values

Before writing down your test, it might be great to confirm that the arbitrary you will be using produce the values you want.
Expand Down
26 changes: 23 additions & 3 deletions src/check/arbitrary/ArrayArbitrary.ts
@@ -1,5 +1,6 @@
import { Random } from '../../random/generator/Random';
import { Stream } from '../../stream/Stream';
import { cloneMethod } from '../symbols';
import { Arbitrary } from './definition/Arbitrary';
import { ArbitraryWithShrink } from './definition/ArbitraryWithShrink';
import { biasWrapper } from './definition/BiasedArbitraryWrapper';
Expand All @@ -18,11 +19,30 @@ class ArrayArbitrary<T> extends Arbitrary<T[]> {
super();
this.lengthArb = integer(minLength, maxLength);
}
private static makeItCloneable<T>(vs: T[], shrinkables: Shrinkable<T>[]) {
(vs as any)[cloneMethod] = () => {
const cloned = [];
for (let idx = 0; idx !== shrinkables.length; ++idx) {
cloned.push(shrinkables[idx].value); // push potentially cloned values
}
this.makeItCloneable(cloned, shrinkables);
return cloned;
};
return vs;
}
private wrapper(itemsRaw: Shrinkable<T>[], shrunkOnce: boolean): Shrinkable<T[]> {
const items = this.preFilter(itemsRaw);
return new Shrinkable(items.map(s => s.value), () =>
this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(v, true))
);
let cloneable = false;
const vs = [];
for (let idx = 0; idx !== items.length; ++idx) {
const s = items[idx];
cloneable = cloneable || s.hasToBeCloned;
vs.push(s.value); // TODO: it might be possible not to clone some values
}
if (cloneable) {
ArrayArbitrary.makeItCloneable(vs, items);
}
return new Shrinkable(vs, () => this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(v, true)));
}
generate(mrng: Random): Shrinkable<T[]> {
const size = this.lengthArb.generate(mrng);
Expand Down
44 changes: 44 additions & 0 deletions src/check/arbitrary/ContextArbitrary.ts
@@ -0,0 +1,44 @@
import { cloneMethod } from '../symbols';
import { constant } from './ConstantArbitrary';
import { Arbitrary } from './definition/Arbitrary';

/**
* Execution context attached to one predicate run
*/
export interface Context {
/**
* Log execution details during a test.
* Very helpful when troubleshooting failures
* @param data Data to be logged into the current context
*/
log(data: string): void;
/**
* Number of logs already logged into current context
*/
size(): number;
}

/** @hidden */
class ContextImplem implements Context {
private readonly receivedLogs: string[];
constructor() {
this.receivedLogs = [];
}
log(data: string): void {
this.receivedLogs.push(data);
}
size(): number {
return this.receivedLogs.length;
}
toString() {
return JSON.stringify({ logs: this.receivedLogs });
}
[cloneMethod]() {
return new ContextImplem();
}
}

/**
* Produce a {@link Context} instance
*/
export const context = () => constant(new ContextImplem()) as Arbitrary<Context>;
2 changes: 1 addition & 1 deletion src/check/arbitrary/OptionArbitrary.ts
Expand Up @@ -14,7 +14,7 @@ class OptionArbitrary<T> extends Arbitrary<T | null> {
function* g(): IterableIterator<Shrinkable<T | null>> {
yield new Shrinkable(null);
}
return new Shrinkable(s.value, () =>
return new Shrinkable(s.value_, () =>
s
.shrink()
.map(OptionArbitrary.extendedShrinkable)
Expand Down
2 changes: 1 addition & 1 deletion src/check/arbitrary/SetArbitrary.ts
Expand Up @@ -22,7 +22,7 @@ function buildCompareFilter<T>(compare: (a: T, b: T) => boolean): ((tab: Shrinka
return (tab: Shrinkable<T>[]): Shrinkable<T>[] => {
let finalLength = tab.length;
for (let idx = tab.length - 1; idx !== -1; --idx) {
if (subArrayContains(tab, idx, t => compare(t.value, tab[idx].value))) {
if (subArrayContains(tab, idx, t => compare(t.value_, tab[idx].value_))) {
--finalLength;
swap(tab, idx, finalLength);
}
Expand Down
26 changes: 23 additions & 3 deletions src/check/arbitrary/TupleArbitrary.generic.ts
@@ -1,5 +1,6 @@
import { Random } from '../../random/generator/Random';
import { Stream } from '../../stream/Stream';
import { cloneMethod } from '../symbols';
import { Arbitrary } from './definition/Arbitrary';
import { Shrinkable } from './definition/Shrinkable';

Expand All @@ -13,10 +14,29 @@ class GenericTupleArbitrary<Ts> extends Arbitrary<Ts[]> {
throw new Error(`Invalid parameter encountered at index ${idx}: expecting an Arbitrary`);
}
}
private static makeItCloneable<Ts>(vs: Ts[], shrinkables: Shrinkable<Ts>[]) {
(vs as any)[cloneMethod] = () => {
const cloned = [];
for (let idx = 0; idx !== shrinkables.length; ++idx) {
cloned.push(shrinkables[idx].value); // push potentially cloned values
}
GenericTupleArbitrary.makeItCloneable(cloned, shrinkables);
return cloned;
};
return vs;
}
private static wrapper<Ts>(shrinkables: Shrinkable<Ts>[]): Shrinkable<Ts[]> {
return new Shrinkable(shrinkables.map(s => s.value), () =>
GenericTupleArbitrary.shrinkImpl(shrinkables).map(GenericTupleArbitrary.wrapper)
);
let cloneable = false;
const vs = [];
for (let idx = 0; idx !== shrinkables.length; ++idx) {
const s = shrinkables[idx];
cloneable = cloneable || s.hasToBeCloned;
vs.push(s.value); // TODO: it might be possible not to clone some values
}
if (cloneable) {
GenericTupleArbitrary.makeItCloneable(vs, shrinkables);
}
return new Shrinkable(vs, () => GenericTupleArbitrary.shrinkImpl(shrinkables).map(GenericTupleArbitrary.wrapper));
}
generate(mrng: Random): Shrinkable<Ts[]> {
return GenericTupleArbitrary.wrapper(this.arbs.map(a => a.generate(mrng)));
Expand Down
40 changes: 38 additions & 2 deletions src/check/arbitrary/definition/Shrinkable.ts
@@ -1,15 +1,51 @@
import { Stream } from '../../../stream/Stream';
import { cloneMethod, hasCloneMethod, WithCloneMethod } from '../../symbols';

/**
* A Shrinkable<T> holds an internal value of type `T`
* and can shrink it to smaller `T` values
*/
export class Shrinkable<T> {
/**
* State storing the result of hasCloneMethod
* If <true> the value will be cloned each time it gets accessed
*/
readonly hasToBeCloned: boolean;
/**
* Safe value of the shrinkable
* Depending on {@link hasToBeCloned} it will either be {@link value_} or a clone of it
*/
readonly value: T;

/**
* @param value Internal value of the shrinkable
* @param shrink Function producing Stream of shrinks associated to value
*/
constructor(readonly value: T, readonly shrink: () => Stream<Shrinkable<T>> = () => Stream.nil<Shrinkable<T>>()) {}
// tslint:disable-next-line:variable-name
constructor(readonly value_: T, readonly shrink: () => Stream<Shrinkable<T>> = () => Stream.nil<Shrinkable<T>>()) {
this.hasToBeCloned = hasCloneMethod(value_);
Object.defineProperty(this, 'value', { get: this.getValue });
}

/** @hidden */
private getValue() {
if (this.hasToBeCloned) {
return ((this.value_ as unknown) as WithCloneMethod<T>)[cloneMethod]();
}
return this.value_;
}

/** @hidden */
private applyMapper<U>(mapper: (t: T) => U): U {
if (this.hasToBeCloned) {
const out = mapper(this.value);
if (out instanceof Object) {
(out as any)[cloneMethod] = () => mapper(this.value);
}
return out;
}
return mapper(this.value);
}

/**
* Create another shrinkable by mapping all values using the provided `mapper`
Expand All @@ -19,7 +55,7 @@ export class Shrinkable<T> {
* @returns New shrinkable with mapped elements
*/
map<U>(mapper: (t: T) => U): Shrinkable<U> {
return new Shrinkable<U>(mapper(this.value), () => this.shrink().map(v => v.map<U>(mapper)));
return new Shrinkable<U>(this.applyMapper(mapper), () => this.shrink().map(v => v.map<U>(mapper)));
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/check/runner/Runner.ts
Expand Up @@ -36,9 +36,9 @@ function runIt<Ts>(
done = true;
let idx = 0;
for (const v of values) {
const out = property.run(v.value) as PreconditionFailure | string | null;
const out = property.run(v.value_) as PreconditionFailure | string | null;
if (out != null && typeof out === 'string') {
runExecution.fail(v.value, idx, out);
runExecution.fail(v.value_, idx, out);
values = v.shrink();
done = false;
break;
Expand Down Expand Up @@ -79,9 +79,9 @@ async function asyncRunIt<Ts>(
done = true;
let idx = 0;
for (const v of values) {
const out = await property.run(v.value);
const out = await property.run(v.value_);
if (out != null && typeof out === 'string') {
runExecution.fail(v.value, idx, out);
runExecution.fail(v.value_, idx, out);
values = v.shrink();
done = false;
break;
Expand Down
25 changes: 25 additions & 0 deletions src/check/symbols.ts
@@ -0,0 +1,25 @@
/**
* Generated instances having a method [cloneMethod]
* will be automatically cloned whenever necessary
*
* This is pretty useful for statefull generated values.
* For instance, whenever you use a Stream you directly impact it.
* Implementing [cloneMethod] on the generated Stream would force
* the framework to clone it whenever it has to re-use it
* (mainly required for chrinking process)
*/
export const cloneMethod = Symbol.for('fast-check/cloneMethod');

/** @hidden */
export interface WithCloneMethod<T> {
[cloneMethod]: () => T;
}

/** @hidden */
export const hasCloneMethod = <T>(instance: T | WithCloneMethod<T>): instance is WithCloneMethod<T> => {
// Valid values for `instanceof Object`:
// [], {}, () => {}, function() {}, async () => {}, async function() {}
// Invalid ones:
// 1, "", Symbol(), null, undefined
return instance instanceof Object && typeof (instance as any)[cloneMethod] === 'function';
};
5 changes: 5 additions & 0 deletions src/fast-check-default.ts
Expand Up @@ -10,6 +10,7 @@ import { array } from './check/arbitrary/ArrayArbitrary';
import { boolean } from './check/arbitrary/BooleanArbitrary';
import { ascii, base64, char, char16bits, fullUnicode, hexa, unicode } from './check/arbitrary/CharacterArbitrary';
import { constant, constantFrom } from './check/arbitrary/ConstantArbitrary';
import { context, Context } from './check/arbitrary/ContextArbitrary';
import { Arbitrary } from './check/arbitrary/definition/Arbitrary';
import { Shrinkable } from './check/arbitrary/definition/Shrinkable';
import { dictionary } from './check/arbitrary/DictionaryArbitrary';
Expand Down Expand Up @@ -52,6 +53,7 @@ import { asyncModelRun, modelRun } from './check/model/ModelRunner';

import { Random } from './random/generator/Random';

import { cloneMethod } from './check/symbols';
import { Stream, stream } from './stream/Stream';

// boolean
Expand Down Expand Up @@ -117,6 +119,7 @@ export {
compareBooleanFunc,
compareFunc,
func,
context,
// model-based
AsyncCommand,
Command,
Expand All @@ -127,7 +130,9 @@ export {
// extend the framework
Arbitrary,
Shrinkable,
cloneMethod,
// interfaces
Context,
ObjectConstraints,
Parameters,
RecordConstraints,
Expand Down

0 comments on commit 7ffc3e6

Please sign in to comment.