Skip to content

Commit

Permalink
Handle unpure generated values (1st commit)
Browse files Browse the repository at this point in the history
Related to #228, #156
Pre-requisite for #222
  • Loading branch information
dubzzz committed Nov 3, 2018
1 parent 2fd8aae commit 2190d73
Show file tree
Hide file tree
Showing 14 changed files with 414 additions and 13 deletions.
26 changes: 23 additions & 3 deletions src/check/arbitrary/ArrayArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ArbitraryWithShrink } from './definition/ArbitraryWithShrink';
import { biasWrapper } from './definition/BiasedArbitraryWrapper';
import { Shrinkable } from './definition/Shrinkable';
import { integer } from './IntegerArbitrary';
import { cloneMethod } from '../symbols';

/** @hidden */
class ArrayArbitrary<T> extends Arbitrary<T[]> {
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { cloneMethod } from '../symbols';
import { constant } from './ConstantArbitrary';
import { Arbitrary } from './definition/Arbitrary';

/**
* Interface for IContext instances
*/
export interface IContext {
/**
* 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 IContext {
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 IContext} instance
*/
export const context = () => constant(new ContextImplem()) as Arbitrary<IContext>;
2 changes: 1 addition & 1 deletion src/check/arbitrary/OptionArbitrary.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Random } from '../../random/generator/Random';
import { Stream } from '../../stream/Stream';
import { Arbitrary } from './definition/Arbitrary';
import { Shrinkable } from './definition/Shrinkable';
import { cloneMethod } from '../symbols';

/** @hidden */
class GenericTupleArbitrary<Ts> extends Arbitrary<Ts[]> {
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
25 changes: 24 additions & 1 deletion src/check/arbitrary/definition/Shrinkable.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
import { Stream } from '../../../stream/Stream';
import { hasCloneMethod, WithCloneMethod, cloneMethod } 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>>()) {}
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_;
}

/**
* Create another shrinkable by mapping all values using the provided `mapper`
Expand Down
8 changes: 4 additions & 4 deletions src/check/runner/Runner.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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, IContext } 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 @@ -53,6 +54,7 @@ import { asyncModelRun, modelRun } from './check/model/ModelRunner';
import { Random } from './random/generator/Random';

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

// boolean
// floating point types
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
IContext,
ObjectConstraints,
Parameters,
RecordConstraints,
Expand Down
Loading

0 comments on commit 2190d73

Please sign in to comment.