Skip to content

Commit

Permalink
Better handling of commands
Browse files Browse the repository at this point in the history
Fixes #230
  • Loading branch information
dubzzz committed Nov 3, 2018
1 parent 307e418 commit ac3112d
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 27 deletions.
6 changes: 0 additions & 6 deletions src/check/model/ModelRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ const internalModelRun = <Model extends object, Real>(
try {
return genericModelRun(s, cmds, undefined, then);
} catch (err) {
if ('errorDetected' in cmds && typeof cmds.errorDetected === 'function') {
cmds.errorDetected();
}
throw err;
}
};
Expand All @@ -49,9 +46,6 @@ const internalAsyncModelRun = async <Model extends object, Real>(
try {
return await genericModelRun(s, cmds, Promise.resolve(), then);
} catch (err) {
if ('errorDetected' in cmds && typeof cmds.errorDetected === 'function') {
cmds.errorDetected();
}
throw err;
}
};
Expand Down
21 changes: 9 additions & 12 deletions src/check/model/commands/CommandsArbitrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,28 @@ class CommandsArbitrary<Model extends object, Real, RunResult> extends Arbitrary
this.oneCommandArb = oneof(...commandArbs).map(c => new CommandWrapper(c));
this.lengthArb = nat(maxCommands);
}
private static cloneCommands<Model extends object, Real, RunResult>(
cmds: Shrinkable<CommandWrapper<Model, Real, RunResult>>[]
) {
return cmds.map(c => new Shrinkable(c.value.clone(), c.shrink));
}
private wrapper(
items: Shrinkable<CommandWrapper<Model, Real, RunResult>>[],
shrunkOnce: boolean
): Shrinkable<CommandsIterable<Model, Real, RunResult>> {
return new Shrinkable(new CommandsIterable(items.map(s => s.value)), () =>
this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(CommandsArbitrary.cloneCommands(v), true))
return new Shrinkable(new CommandsIterable(items.map(s => s.value_)), () =>
this.shrinkImpl(items, shrunkOnce).map(v => this.wrapper(v, true))
);
}
generate(mrng: Random): Shrinkable<CommandsIterable<Model, Real, RunResult>> {
const size = this.lengthArb.generate(mrng);
const items: Shrinkable<CommandWrapper<Model, Real, RunResult>>[] = Array(size.value);
for (let idx = 0; idx !== size.value; ++idx) {
items[idx] = this.oneCommandArb.generate(mrng);
const items: Shrinkable<CommandWrapper<Model, Real, RunResult>>[] = Array(size.value_);
for (let idx = 0; idx !== size.value_; ++idx) {
const item = this.oneCommandArb.generate(mrng);
items[idx] = item;
}
return this.wrapper(items, false);
}
private shrinkImpl(
itemsRaw: Shrinkable<CommandWrapper<Model, Real, RunResult>>[],
shrunkOnce: boolean
): Stream<Shrinkable<CommandWrapper<Model, Real, RunResult>>[]> {
const items = itemsRaw.filter(c => c.value.hasRan); // filter out commands that have not been executed
const items = itemsRaw.filter(c => c.value_.hasRan); // filter out commands that have not been executed
if (items.length === 0) {
return Stream.nil<Shrinkable<CommandWrapper<Model, Real, RunResult>>[]>();
}
Expand All @@ -58,7 +54,8 @@ class CommandsArbitrary<Model extends object, Real, RunResult> extends Arbitrary
return emptyOrNil
.join(size.shrink().map(l => items.slice(0, l.value).concat(items[items.length - 1]))) // try: remove items except the last one
.join(this.shrinkImpl(items.slice(0, items.length - 1), false).map(vs => vs.concat(items[items.length - 1]))) // try: keep last, shrink remaining (rec)
.join(items[0].shrink().map(v => [v].concat(items.slice(1)))); // try: shrink first, keep others
.join(items[0].shrink().map(v => [v].concat(items.slice(1)))) // try: shrink first, keep others
.map(shrinkables => shrinkables.map(s => new Shrinkable(s.value_.clone(), s.shrink)));
}
}

Expand Down
15 changes: 6 additions & 9 deletions src/check/model/commands/CommandsIterable.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { cloneMethod } from '../../symbols';
import { CommandWrapper } from './CommandWrapper';

/** @hidden */
export class CommandsIterable<Model extends object, Real, RunResult>
implements Iterable<CommandWrapper<Model, Real, RunResult>> {
private lastErrorDetectedStr: string = '';
constructor(readonly commands: CommandWrapper<Model, Real, RunResult>[]) {}
[Symbol.iterator](): Iterator<CommandWrapper<Model, Real, RunResult>> {
for (let idx = 0; idx !== this.commands.length; ++idx) {
this.commands[idx].hasRan = false;
}
return this.commands[Symbol.iterator]();
}
errorDetected() {
this.lastErrorDetectedStr = this.commands
[cloneMethod]() {
return new CommandsIterable(this.commands.map(c => c.clone()));
}
toString(): string {
return this.commands
.filter(c => c.hasRan)
.map(c => c.toString())
.join(',');
}
toString(): string {
return this.lastErrorDetectedStr;
}
}
15 changes: 15 additions & 0 deletions test/unit/check/model/commands/CommandsArbitrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ describe('CommandWrapper', () => {
}
}
};
it('Should generate a cloneable shrinkable', () =>
fc.assert(
fc.property(fc.integer(), (seed: number) => {
const mrng = new Random(prand.xorshift128plus(seed));
let logOnCheck: { data: string[] } = { data: [] };

const baseCommands = commands([
constant(new SuccessCommand(logOnCheck)),
constant(new SkippedCommand(logOnCheck)),
constant(new FailureCommand(logOnCheck))
]).generate(mrng);

return baseCommands.hasToBeCloned;
})
));
it('Should skip skipped commands on shrink', () =>
fc.assert(
fc.property(fc.integer(), (seed: number) => {
Expand Down
68 changes: 68 additions & 0 deletions test/unit/check/model/commands/CommandsIterable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as assert from 'assert';
import * as fc from '../../../../../lib/fast-check';

import { CommandWrapper } from '../../../../../src/check/model/commands/CommandWrapper';
import { CommandsIterable } from '../../../../../src/check/model/commands/CommandsIterable';
import { Command } from '../../../../../src/check/model/command/Command';
import { cloneMethod } from '../../../../../src/check/symbols';

type Model = {};
type Real = {};

const buildAlreadyRanCommands = (runFlags: boolean[]) => {
return runFlags.map((hasRun, idx) => {
const cmd = new class implements Command<Model, Real> {
check = (m: Readonly<Model>) => true;
run = (m: Model, r: Real) => {};
toString = () => String(idx);
}();
const wrapper = new CommandWrapper(cmd);
if (hasRun) {
wrapper.run({}, {});
}
return wrapper;
});
};

describe('CommandsIterable', () => {
it('Should not reset hasRun flag on iteration', () =>
fc.assert(
fc.property(fc.array(fc.boolean()), runFlags => {
const commands = [...new CommandsIterable(buildAlreadyRanCommands(runFlags))];
for (let idx = 0; idx !== runFlags.length; ++idx) {
assert.equal(commands[idx].hasRan, runFlags[idx]);
}
})
));
it('Should not reset hasRun flag on the original iterable on clone', () =>
fc.assert(
fc.property(fc.array(fc.boolean()), runFlags => {
const originalIterable = new CommandsIterable(buildAlreadyRanCommands(runFlags));
originalIterable[cloneMethod]();
const commands = [...originalIterable];
for (let idx = 0; idx !== runFlags.length; ++idx) {
assert.equal(commands[idx].hasRan, runFlags[idx]);
}
})
));
it('Should reset hasRun flag for the clone on clone', () =>
fc.assert(
fc.property(fc.array(fc.boolean()), runFlags => {
const commands = [...new CommandsIterable(buildAlreadyRanCommands(runFlags))[cloneMethod]()];
for (let idx = 0; idx !== runFlags.length; ++idx) {
assert.ok(!commands[idx].hasRan);
}
})
));
it('Should only print ran commands', () =>
fc.assert(
fc.property(fc.array(fc.boolean()), runFlags => {
const commandsIterable = new CommandsIterable(buildAlreadyRanCommands(runFlags));
const expectedToString = runFlags
.map((hasRan, idx) => (hasRan ? String(idx) : ''))
.filter(s => s !== '')
.join(',');
assert.equal(commandsIterable.toString(), expectedToString);
})
));
});

0 comments on commit ac3112d

Please sign in to comment.