-
-
Notifications
You must be signed in to change notification settings - Fork 175
/
CommandsArbitrary.spec.ts
173 lines (167 loc) · 6.05 KB
/
CommandsArbitrary.spec.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import * as fc from '../../../src/fast-check';
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 += this.n;
};
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 -= this.n;
};
toString = () => `dec[${this.n}]`;
}
class EvenCommand implements fc.Command<M1, R1> {
check = (m: Readonly<M1>) => m.count % 2 === 0;
run = (m: M1, r: R1) => {};
toString = () => 'even';
}
class OddCommand implements fc.Command<M1, R1> {
check = (m: Readonly<M1>) => m.count % 2 !== 0;
run = (m: M1, r: R1) => {};
toString = () => 'odd';
}
class CheckLessThanCommand implements fc.Command<M1, R1> {
constructor(readonly lessThanValue: number) {}
check = (m: Readonly<M1>) => true;
run = (m: M1, r: R1) => {
expect(m.count).toBeLessThan(this.lessThanValue);
};
toString = () => `check[${this.lessThanValue}]`;
}
class SuccessAlwaysCommand implements fc.Command<M1, R1> {
check = (m: Readonly<M1>) => true;
run = (m: M1, r: R1) => {};
toString = () => 'success';
}
type M2 = {
current: { stepId: number };
validSteps: number[];
};
type R2 = {};
class SuccessCommand implements fc.Command<M2, R2> {
check = (m: Readonly<M2>) => m.validSteps.includes(m.current.stepId++);
run = (m: M2, r: R2) => {};
toString = () => 'success';
}
class FailureCommand implements fc.Command<M2, R2> {
check = (m: Readonly<M2>) => m.validSteps.includes(m.current.stepId++);
run = (m: M2, r: R2) => {
throw 'failure';
};
toString = () => 'failure';
}
const seed = Date.now();
describe(`CommandsArbitrary (seed: ${seed})`, () => {
describe('commands', () => {
it('Should shrink up to the shortest failing commands list', () => {
const out = fc.check(
fc.property(
fc.commands(
[
fc.nat().map(n => new IncreaseCommand(n)),
fc.nat().map(n => new DecreaseCommand(n)),
fc.constant(new EvenCommand()),
fc.constant(new OddCommand()),
fc.nat().map(n => new CheckLessThanCommand(n + 1))
],
{ disableReplayLog: true, maxCommands: 1000 }
),
cmds => {
const setup = () => ({
model: { count: 0 },
real: {}
});
fc.modelRun(setup, cmds);
}
),
{ seed: seed }
);
expect(out.failed).toBe(true);
const cmdsRepr = out.counterexample![0].toString();
expect(cmdsRepr).toMatch(/check\[(\d+)\]$/);
expect(cmdsRepr).toEqual('inc[1],check[1]');
});
it('Should result in empty commands if failures happen after the run', () => {
const out = fc.check(
fc.property(fc.commands([fc.constant(new SuccessAlwaysCommand())]), cmds => {
const setup = () => ({
model: { count: 0 },
real: {}
});
fc.modelRun(setup, cmds);
return false; // fails after the model, no matter the commands
}),
{ seed: seed }
);
expect(out.failed).toBe(true);
expect([...out.counterexample![0]]).toHaveLength(0);
});
it('Should shrink towards minimal case even with other arbitraries', () => {
// Why this test?
//
// fc.commands is one of the rare Arbitrary relying on an internal state.
// By generating commands along with other arbitraries, we could highlight states issues.
// Basically shrinking will re-use the generated commands multiple times along with a shrunk array.
//
// First version was failing on this test with the following output:
// Expected the only played command to be 'failure', got: -,success,failure for steps 2
// The output for 'steps 2' should have been '-,-,failure'
const out = fc.check(
fc.property(
fc.array(fc.nat(9), 0, 3),
fc.commands([fc.constant(new FailureCommand()), fc.constant(new SuccessCommand())], {
disableReplayLog: true
}),
fc.array(fc.nat(9), 0, 3),
(validSteps1, cmds, validSteps2) => {
const setup = () => ({
model: { current: { stepId: 0 }, validSteps: [...validSteps1, ...validSteps2] },
real: {}
});
fc.modelRun(setup, cmds);
}
),
{ seed: seed }
);
expect(out.failed).toBe(true);
expect(out.counterexample![1].toString()).toEqual('failure');
});
it('Should not start a run with already started commands', () => {
// Why this test?
// fc.commands relies on cloning not to waste the hasRan status of an execution
// between two runs it is supposed to clone the commands before resetting the hasRan flag
let unexpectedPartiallyExecuted: string[] = [];
const out = fc.check(
fc.property(
fc.array(fc.nat(9), 0, 3),
fc.commands([fc.constant(new FailureCommand()), fc.constant(new SuccessCommand())], {
disableReplayLog: true
}),
fc.array(fc.nat(9), 0, 3),
(validSteps1, cmds, validSteps2) => {
if (String(cmds) !== '') {
// When no command has been started, String(cmds) === ''
// Having String(cmds) !== '' implies that some commands have the hasRan flag ON
unexpectedPartiallyExecuted.push(String(cmds));
}
const setup = () => ({
model: { current: { stepId: 0 }, validSteps: [...validSteps1, ...validSteps2] },
real: {}
});
fc.modelRun(setup, cmds);
}
),
{ seed: seed }
);
expect(out.failed).toBe(true);
expect(unexpectedPartiallyExecuted).toEqual([]);
});
});
});