Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: Not shrinking commands themselves #295

Merged
merged 1 commit into from
Jan 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions src/check/model/commands/CommandsArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ class CommandsArbitrary<Model extends object, Real, RunResult, CheckAsync extend
}
private wrapper(
items: Shrinkable<CommandWrapper<Model, Real, RunResult, CheckAsync>>[],
shrunkOnce: boolean,
alwaysKeepOneCommand: boolean
shrunkOnce: boolean
): Shrinkable<CommandsIterable<Model, Real, RunResult, CheckAsync>> {
return new Shrinkable(new CommandsIterable(items.map(s => s.value_)), () =>
this.shrinkImpl(items, shrunkOnce, alwaysKeepOneCommand).map(v => this.wrapper(v, true, true))
this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(v, true))
);
}
generate(mrng: Random): Shrinkable<CommandsIterable<Model, Real, RunResult, CheckAsync>> {
Expand All @@ -38,12 +37,11 @@ class CommandsArbitrary<Model extends object, Real, RunResult, CheckAsync extend
const item = this.oneCommandArb.generate(mrng);
items[idx] = item;
}
return this.wrapper(items, false, false);
return this.wrapper(items, false);
}
private shrinkImpl(
itemsRaw: Shrinkable<CommandWrapper<Model, Real, RunResult, CheckAsync>>[],
shrunkOnce: boolean,
alwaysKeepOneCommand: boolean
shrunkOnce: boolean
): Stream<Shrinkable<CommandWrapper<Model, Real, RunResult, CheckAsync>>[]> {
const items = itemsRaw.filter(c => c.value_.hasRan); // filter out commands that have not been executed
if (items.length === 0) {
Expand All @@ -52,20 +50,32 @@ class CommandsArbitrary<Model extends object, Real, RunResult, CheckAsync extend

// The shrinker of commands have to keep the last item
// because it is the one causing the failure
const emptyOrNil = alwaysKeepOneCommand
let allShrinks = shrunkOnce
? Stream.nil<Shrinkable<CommandWrapper<Model, Real, RunResult, CheckAsync>>[]>()
: new Stream([[]][Symbol.iterator]());
const size = this.lengthArb.shrinkableFor(items.length - 1, shrunkOnce);

return emptyOrNil
.join(size.shrink().map(l => items.slice(items.length - (l.value + 1)))) // try: remove items except the last one
.join(this.shrinkImpl(items.slice(1), false, true).map(vs => [items[0]].concat(vs))) // try: keep first, shrink remaining (rec)
.join(items[items.length - 1].shrink().map(v => items.slice(0, -1).concat([v]))) // try: shrink last, keep others
.map(shrinkables => {
return shrinkables.map(c => {
return new Shrinkable(c.value_.clone(), c.shrink);
});
// keep fixed number commands at the beginnign
// remove items in remaining part except the last one
for (let numToKeep = 0; numToKeep !== items.length; ++numToKeep) {
const size = this.lengthArb.shrinkableFor(items.length - 1 - numToKeep, false);
const fixedStart = items.slice(0, numToKeep);
allShrinks = allShrinks.join(
size.shrink().map(l => fixedStart.concat(items.slice(items.length - (l.value + 1))))
);
}

// shrink one by one
for (let itemAt = 0; itemAt !== items.length; ++itemAt) {
allShrinks = allShrinks.join(
items[itemAt].shrink().map(v => items.slice(0, itemAt).concat([v], items.slice(itemAt + 1)))
);
}

return allShrinks.map(shrinkables => {
return shrinkables.map(c => {
return new Shrinkable(c.value_.clone(), c.shrink);
});
});
}
}

Expand Down
18 changes: 10 additions & 8 deletions test/e2e/model/CommandsArbitrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ type M1 = { count: number };
type R1 = {};

class IncreaseCommand implements fc.Command<M1, R1> {
constructor(readonly n: number) {}
check = (m: Readonly<M1>) => true;
run = (m: M1, r: R1) => {
m.count += 1;
m.count += this.n;
};
toString = () => 'inc';
toString = () => `inc[${this.n}]`;
}
class DecreaseCommand implements fc.Command<M1, R1> {
constructor(readonly n: number) {}
check = (m: Readonly<M1>) => true;
run = (m: M1, r: R1) => {
m.count -= 1;
m.count -= this.n;
};
toString = () => 'dec';
toString = () => `dec[${this.n}]`;
}
class EvenCommand implements fc.Command<M1, R1> {
check = (m: Readonly<M1>) => m.count % 2 === 0;
Expand Down Expand Up @@ -68,11 +70,11 @@ describe(`CommandsArbitrary (seed: ${seed})`, () => {
fc.property(
fc.commands(
[
fc.constant(new IncreaseCommand()),
fc.constant(new DecreaseCommand()),
fc.nat().map(n => new IncreaseCommand(n)),
fc.nat().map(n => new DecreaseCommand(n)),
fc.constant(new EvenCommand()),
fc.constant(new OddCommand()),
fc.integer(1, 10).map(v => new CheckLessThanCommand(v))
fc.nat().map(n => new CheckLessThanCommand(n + 1))
],
1000
),
Expand All @@ -90,7 +92,7 @@ describe(`CommandsArbitrary (seed: ${seed})`, () => {

const cmdsRepr = out.counterexample![0].toString();
expect(cmdsRepr).toMatch(/check\[(\d+)\]$/);
expect(cmdsRepr).toEqual('inc,check[1]');
expect(cmdsRepr).toEqual('inc[1],check[1]');
});
it('Should result in empty commands if failures happen after the run', () => {
const out = fc.check(
Expand Down