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

Better handling of commands #235

Merged
merged 1 commit into from
Nov 3, 2018
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
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);
})
));
});