Skip to content

Commit 963f5cf

Browse files
committed
Refactor how tests are actually run
Fixes #1684. Fixes #1416. Refs #1158. Properly support serial hooks. Hooks are divided into the following categories: * before * beforeEach * afterEach * afterEach.always * after * after.always For each category all hooks are run in the order they're declared in. This is different from tests, where serial tests are run before concurrent ones. By default hooks run concurrently. However a serial hook is not run before all preceding concurrent hooks have completed. Serial hooks are never run concurrently. Always hooks are now always run, even if --fail-fast is enabled. Internally, TestCollection, Sequence and Concurrent have been removed. This has led to a major refactor of Runner, and some smaller breaking changes and bug fixes: * Unnecessary validations have been removed * Macros can be used with hooks * A macro is recognized even if no additional arguments are given, so it can modify the test (or hook) title * --match now also matches todo tests * Skipped and todo tests are shown first in the output * --fail-fast prevents subsequent tests from running as long as the failure occurs in a serial test
1 parent 8de2630 commit 963f5cf

24 files changed

+1456
-3627
lines changed

index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ export interface SerialInterface<Context = {}> {
168168
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
169169
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
170170

171+
after: AfterInterface<Context>;
172+
afterEach: AfterInterface<Context>;
173+
before: BeforeInterface<Context>;
174+
beforeEach: BeforeInterface<Context>;
171175
cb: CbInterface<Context>;
172176
failing: FailingInterface<Context>;
173177
only: OnlyInterface<Context>;

index.js.flow

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ export interface SerialInterface<Context = {}> {
171171
(title: string, macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
172172
(macro: Macro<Context> | Macro<Context>[], ...args: Array<any>): void;
173173

174+
after: AfterInterface<Context>;
175+
afterEach: AfterInterface<Context>;
176+
before: BeforeInterface<Context>;
177+
beforeEach: BeforeInterface<Context>;
174178
cb: CbInterface<Context>;
175179
failing: FailingInterface<Context>;
176180
only: OnlyInterface<Context>;

lib/concurrent.js

Lines changed: 0 additions & 64 deletions
This file was deleted.

lib/context-ref.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
const clone = require('lodash.clone');
3+
4+
class ContextRef {
5+
constructor() {
6+
this.value = {};
7+
}
8+
9+
get() {
10+
return this.value;
11+
}
12+
13+
set(newValue) {
14+
this.value = newValue;
15+
}
16+
17+
copy() {
18+
return new LateBinding(this); // eslint-disable-line no-use-before-define
19+
}
20+
}
21+
module.exports = ContextRef;
22+
23+
class LateBinding extends ContextRef {
24+
constructor(ref) {
25+
super();
26+
this.ref = ref;
27+
this.bound = false;
28+
}
29+
30+
get() {
31+
if (!this.bound) {
32+
this.set(clone(this.ref.get()));
33+
}
34+
return super.get();
35+
}
36+
37+
set(newValue) {
38+
this.bound = true;
39+
super.set(newValue);
40+
}
41+
}

lib/create-chain.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use strict';
2+
const chainRegistry = new WeakMap();
3+
4+
function startChain(name, call, defaults) {
5+
const fn = function () {
6+
call(Object.assign({}, defaults), Array.from(arguments));
7+
};
8+
Object.defineProperty(fn, 'name', {value: name});
9+
chainRegistry.set(fn, {call, defaults, fullName: name});
10+
return fn;
11+
}
12+
13+
function extendChain(prev, name, flag) {
14+
if (!flag) {
15+
flag = name;
16+
}
17+
18+
const fn = function () {
19+
callWithFlag(prev, flag, Array.from(arguments));
20+
};
21+
const fullName = `${chainRegistry.get(prev).fullName}.${name}`;
22+
Object.defineProperty(fn, 'name', {value: fullName});
23+
prev[name] = fn;
24+
25+
chainRegistry.set(fn, {flag, fullName, prev});
26+
return fn;
27+
}
28+
29+
function callWithFlag(prev, flag, args) {
30+
const combinedFlags = {[flag]: true};
31+
do {
32+
const step = chainRegistry.get(prev);
33+
if (step.call) {
34+
step.call(Object.assign({}, step.defaults, combinedFlags), args);
35+
prev = null;
36+
} else {
37+
combinedFlags[step.flag] = true;
38+
prev = step.prev;
39+
}
40+
} while (prev);
41+
}
42+
43+
function createHookChain(hook, isAfterHook) {
44+
// Hook chaining rules:
45+
// * `always` comes immediately after "after hooks"
46+
// * `skip` must come at the end
47+
// * no `only`
48+
// * no repeating
49+
extendChain(hook, 'cb', 'callback');
50+
extendChain(hook, 'skip', 'skipped');
51+
extendChain(hook.cb, 'skip', 'skipped');
52+
if (isAfterHook) {
53+
extendChain(hook, 'always');
54+
extendChain(hook.always, 'cb', 'callback');
55+
extendChain(hook.always, 'skip', 'skipped');
56+
extendChain(hook.always.cb, 'skip', 'skipped');
57+
}
58+
return hook;
59+
}
60+
61+
function createChain(fn, defaults) {
62+
// Test chaining rules:
63+
// * `serial` must come at the start
64+
// * `only` and `skip` must come at the end
65+
// * `failing` must come at the end, but can be followed by `only` and `skip`
66+
// * `only` and `skip` cannot be chained together
67+
// * no repeating
68+
const root = startChain('test', fn, Object.assign({}, defaults, {type: 'test'}));
69+
extendChain(root, 'cb', 'callback');
70+
extendChain(root, 'failing');
71+
extendChain(root, 'only', 'exclusive');
72+
extendChain(root, 'serial');
73+
extendChain(root, 'skip', 'skipped');
74+
extendChain(root.cb, 'failing');
75+
extendChain(root.cb, 'only', 'exclusive');
76+
extendChain(root.cb, 'skip', 'skipped');
77+
extendChain(root.cb.failing, 'only', 'exclusive');
78+
extendChain(root.cb.failing, 'skip', 'skipped');
79+
extendChain(root.failing, 'only', 'exclusive');
80+
extendChain(root.failing, 'skip', 'skipped');
81+
extendChain(root.serial, 'cb', 'callback');
82+
extendChain(root.serial, 'failing');
83+
extendChain(root.serial, 'only', 'exclusive');
84+
extendChain(root.serial, 'skip', 'skipped');
85+
extendChain(root.serial.cb, 'failing');
86+
extendChain(root.serial.cb, 'only', 'exclusive');
87+
extendChain(root.serial.cb, 'skip', 'skipped');
88+
extendChain(root.serial.cb.failing, 'only', 'exclusive');
89+
extendChain(root.serial.cb.failing, 'skip', 'skipped');
90+
91+
root.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {type: 'after'})), true);
92+
root.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {type: 'afterEach'})), true);
93+
root.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {type: 'before'})), false);
94+
root.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {type: 'beforeEach'})), false);
95+
96+
root.serial.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {serial: true, type: 'after'})), true);
97+
root.serial.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {serial: true, type: 'afterEach'})), true);
98+
root.serial.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {serial: true, type: 'before'})), false);
99+
root.serial.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {serial: true, type: 'beforeEach'})), false);
100+
101+
// "todo" tests cannot be chained. Allow todo tests to be flagged as needing
102+
// to be serial.
103+
root.todo = startChain('test.todo', fn, Object.assign({}, defaults, {type: 'test', todo: true}));
104+
root.serial.todo = startChain('test.serial.todo', fn, Object.assign({}, defaults, {serial: true, type: 'test', todo: true}));
105+
106+
return root;
107+
}
108+
module.exports = createChain;

lib/main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ const Runner = require('./runner');
44
const opts = require('./worker-options').get();
55

66
const runner = new Runner({
7-
bail: opts.failFast,
7+
failFast: opts.failFast,
88
failWithoutAssertions: opts.failWithoutAssertions,
99
file: opts.file,
1010
match: opts.match,
1111
projectDir: opts.projectDir,
12+
runOnlyExclusive: opts.runOnlyExclusive,
1213
serial: opts.serial,
13-
updateSnapshots: opts.updateSnapshots,
1414
snapshotDir: opts.snapshotDir,
15-
runOnlyExclusive: opts.runOnlyExclusive
15+
updateSnapshots: opts.updateSnapshots
1616
});
1717

1818
worker.setRunner(runner);

0 commit comments

Comments
 (0)