Skip to content

Commit

Permalink
Benefit from the no shrink twice towards zero in arrays
Browse files Browse the repository at this point in the history
Fixes #62
Fixes #63
  • Loading branch information
dubzzz committed Apr 6, 2018
1 parent fc57174 commit d1dde51
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 9 deletions.
14 changes: 8 additions & 6 deletions src/check/arbitrary/ArrayArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,30 @@ class ArrayArbitrary<T> extends Arbitrary<T[]> {
super();
this.lengthArb = integer(minLength, maxLength);
}
private wrapper(itemsRaw: Shrinkable<T>[]): Shrinkable<T[]> {
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).map(v => this.wrapper(v)));
return new Shrinkable(items.map(s => s.value), () =>
this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(v, true))
);
}
generate(mrng: Random): Shrinkable<T[]> {
const size = this.lengthArb.generate(mrng);
const items = [...Array(size.value)].map(() => this.arb.generate(mrng));
return this.wrapper(items);
return this.wrapper(items, false);
}
private shrinkImpl(items: Shrinkable<T>[]): Stream<Shrinkable<T>[]> {
private shrinkImpl(items: Shrinkable<T>[], shrunkOnce: boolean): Stream<Shrinkable<T>[]> {
// shrinking one by one is the not the most comprehensive
// but allows a reasonable number of entries in the shrink
if (items.length === 0) {
return Stream.nil<Shrinkable<T>[]>();
}
const size = this.lengthArb.shrinkableFor(items.length);
const size = this.lengthArb.shrinkableFor(items.length, shrunkOnce);
return size
.shrink()
.map(l => items.slice(items.length - l.value))
.join(items[0].shrink().map(v => [v].concat(items.slice(1))))
.join(
this.shrinkImpl(items.slice(1))
this.shrinkImpl(items.slice(1), false)
.filter(vs => this.minLength <= vs.length + 1)
.map(vs => [items[0]].concat(vs))
);
Expand Down
8 changes: 5 additions & 3 deletions src/check/arbitrary/definition/Arbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ export default abstract class Arbitrary<T> {

abstract class ArbitraryWithShrink<T> extends Arbitrary<T> {
abstract generate(mrng: Random): Shrinkable<T>;
abstract shrink(value: T): Stream<T>;
shrinkableFor(value: T): Shrinkable<T> {
return new Shrinkable(value, () => this.shrink(value).map(v => this.shrinkableFor(v)));
abstract shrink(value: T, shrunkOnce?: boolean): Stream<T>;
shrinkableFor(value: T, shrunkOnce?: boolean): Shrinkable<T> {
return new Shrinkable(value, () =>
this.shrink(value, shrunkOnce === true).map(v => this.shrinkableFor(v, shrunkOnce === true))
);
}
}

Expand Down
16 changes: 16 additions & 0 deletions test/e2e/arbitraries/ArrayArbitrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,21 @@ describe(`ArrayArbitrary (seed: ${seed})`, () => {
assert.ok(out.failed, 'Should have failed');
assert.deepEqual(out.counterexample, [[5, 5]], 'Should shrink to counterexample [5,5]');
});
it('Should not suggest multiple times the empty array (after first failure)', () => {
let failedOnce = false;
let numSuggests = 0;
const out = fc.check(
fc.property(fc.array(fc.integer()), (arr: number[]) => {
if (failedOnce && arr.length === 0) ++numSuggests;
if (arr.length === 0) return true;
failedOnce = true;
return false;
}),
{ seed }
);
assert.ok(out.failed, 'Should have failed');
assert.equal(out.counterexample[0].length, 1, 'Should shrink to a counterexample having a single element');
assert.equal(numSuggests, 1, 'Should have suggested [] only once');
});
});
});
18 changes: 18 additions & 0 deletions test/e2e/arbitraries/StringArbitrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,22 @@ describe(`StringArbitrary (seed: ${seed})`, () => {
assert.deepEqual(out.counterexample, ['\ud800'], 'Should shrink to counterexample "\\ud800"');
});
});
describe('string', () => {
it('Should not suggest multiple times the empty string (after first failure)', () => {
let failedOnce = false;
let numSuggests = 0;
const out = fc.check(
fc.property(fc.string(), (s: string) => {
if (failedOnce && s === '') ++numSuggests;
if (s.length === 0) return true;
failedOnce = true;
return false;
}),
{ seed }
);
assert.ok(out.failed, 'Should have failed');
assert.equal(out.counterexample[0].length, 1, 'Should shrink to a counterexample having a single char');
assert.equal(numSuggests, 1, "Should have suggested '' only once");
});
});
});

0 comments on commit d1dde51

Please sign in to comment.