Skip to content

Commit 5fd44cc

Browse files
boneskullcraigtaub
authored andcommitted
add Root Hook Plugins
(documentation will be in another PR) Adds "root hook plugins", a system to define root hooks via files loaded with `--require`. This enables root hooks to work in parallel mode. Because parallel mode runs files in a non-deterministic order, and files do not share a `Mocha` instance, it is not possible to share these hooks with other test files. This change also works well with third-party libraries for Mocha which need the behavior; these can now be trivially consumed by adding `--require` or `require: 'some-library'` in Mocha's config file. The way it works is: 1. When a file is loaded via `--require`, we check to see if that file exports a property named `mochaHooks` (can be multiple files). 1. If it does, we save a reference to the property. 1. After Yargs' validation phase, we use async middleware to execute root hook plugin functions--or if they are objects, just collect them--and we flatten all hooks found into four buckets corresponding to the four hook types. 1. Once `Mocha` is instantiated, if it is given a `rootHooks` option, those hooks are applied to the root suite. This works with parallel tests because we can save a reference to the flattened hooks in each worker process, and a new `Mocha` instance is created with them for each test file. * * * Tangential: - Because a root hook plugin can be defined as an `async` function, I noticed that `utils.type()` does not return `function` for async functions; it returns `asyncfunction`. I've added a (Node-specific, for now) test for this. - `handleRequires` is now `async`, since it will need to be anyway to support ESM and calls to `import()`. - fixed incorrect call to `fs.existsSync()` Ref: #4198
1 parent e07cf0f commit 5fd44cc

15 files changed

+507
-29
lines changed

lib/cli/run-helpers.js

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ const path = require('path');
1212
const debug = require('debug')('mocha:cli:run:helpers');
1313
const watchRun = require('./watch-run');
1414
const collectFiles = require('./collect-files');
15+
const {type} = require('../utils');
1516
const {format} = require('util');
16-
17-
const cwd = (exports.cwd = process.cwd());
18-
const {createInvalidPluginError} = require('../errors');
17+
const {createInvalidPluginError, createUnsupportedError} = require('../errors');
1918

2019
/**
2120
* Exits Mocha when tests + code under test has finished execution (default)
@@ -75,20 +74,60 @@ exports.list = str =>
7574
Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);
7675

7776
/**
78-
* `require()` the modules as required by `--require <require>`
77+
* `require()` the modules as required by `--require <require>`.
78+
*
79+
* Returns array of `mochaHooks` exports, if any.
7980
* @param {string[]} requires - Modules to require
81+
* @returns {Promise<MochaRootHookObject|MochaRootHookFunction>} Any root hooks
8082
* @private
8183
*/
82-
exports.handleRequires = (requires = []) => {
83-
requires.forEach(mod => {
84+
exports.handleRequires = async (requires = []) =>
85+
requires.reduce((acc, mod) => {
8486
let modpath = mod;
85-
if (fs.existsSync(mod, {cwd}) || fs.existsSync(`${mod}.js`, {cwd})) {
87+
// this is relative to cwd
88+
if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
8689
modpath = path.resolve(mod);
87-
debug('resolved %s to %s', mod, modpath);
90+
debug('resolved required file %s to %s', mod, modpath);
91+
}
92+
const requiredModule = require(modpath);
93+
if (type(requiredModule) === 'object' && requiredModule.mochaHooks) {
94+
const mochaHooksType = type(requiredModule.mochaHooks);
95+
if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') {
96+
debug('found root hooks in required file %s', mod);
97+
acc.push(requiredModule.mochaHooks);
98+
} else {
99+
throw createUnsupportedError(
100+
'mochaHooks must be an object or a function returning (or fulfilling with) an object'
101+
);
102+
}
88103
}
89-
require(modpath);
90104
debug('loaded required module "%s"', mod);
91-
});
105+
return acc;
106+
}, []);
107+
108+
/**
109+
* Loads root hooks as exported via `mochaHooks` from required files.
110+
* These can be sync/async functions returning objects, or just objects.
111+
* Flattens to a single object.
112+
* @param {Array<MochaRootHookObject|MochaRootHookFunction>} rootHooks - Array of root hooks
113+
* @private
114+
* @returns {MochaRootHookObject}
115+
*/
116+
exports.loadRootHooks = async rootHooks => {
117+
const rootHookObjects = await Promise.all(
118+
rootHooks.map(async hook => (/function$/.test(type(hook)) ? hook() : hook))
119+
);
120+
121+
return rootHookObjects.reduce(
122+
(acc, hook) => {
123+
acc.beforeAll = acc.beforeAll.concat(hook.beforeAll || []);
124+
acc.beforeEach = acc.beforeEach.concat(hook.beforeEach || []);
125+
acc.afterAll = acc.afterAll.concat(hook.afterAll || []);
126+
acc.afterEach = acc.afterEach.concat(hook.afterEach || []);
127+
return acc;
128+
},
129+
{beforeAll: [], beforeEach: [], afterAll: [], afterEach: []}
130+
);
92131
};
93132

94133
/**
@@ -106,6 +145,7 @@ const singleRun = async (mocha, {exit}, fileCollectParams) => {
106145
debug('single run with %d file(s)', files.length);
107146
mocha.files = files;
108147

148+
// handles ESM modules
109149
await mocha.loadFilesAsync();
110150
return mocha.run(exit ? exitMocha : exitMochaLater);
111151
};

lib/cli/run.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
list,
1919
handleRequires,
2020
validatePlugin,
21+
loadRootHooks,
2122
runMocha
2223
} = require('./run-helpers');
2324
const {ONE_AND_DONES, ONE_AND_DONE_ARGS} = require('./one-and-dones');
@@ -285,12 +286,24 @@ exports.builder = yargs =>
285286
);
286287
}
287288

289+
if (argv.opts) {
290+
throw createUnsupportedError(
291+
`--opts: configuring Mocha via 'mocha.opts' is DEPRECATED and no longer supported.
292+
Please use a configuration file instead.`
293+
);
294+
}
295+
296+
return true;
297+
})
298+
.middleware(async argv => {
288299
// load requires first, because it can impact "plugin" validation
289-
handleRequires(argv.require);
300+
const rawRootHooks = await handleRequires(argv.require);
290301
validatePlugin(argv, 'reporter', Mocha.reporters);
291302
validatePlugin(argv, 'ui', Mocha.interfaces);
292303

293-
return true;
304+
if (rawRootHooks.length) {
305+
argv.rootHooks = await loadRootHooks(rawRootHooks);
306+
}
294307
})
295308
.array(types.array)
296309
.boolean(types.boolean)

lib/mocha.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ exports.Test = require('./test');
118118
* @param {number} [options.slow] - Slow threshold value.
119119
* @param {number|string} [options.timeout] - Timeout threshold value.
120120
* @param {string} [options.ui] - Interface name.
121+
* @param {MochaRootHookObject} [options.rootHooks] - Hooks to bootstrap the root
122+
* suite with
121123
*/
122124
function Mocha(options) {
123125
options = utils.assign({}, mocharc, options || {});
@@ -165,6 +167,10 @@ function Mocha(options) {
165167
this[opt]();
166168
}
167169
}, this);
170+
171+
if (options.rootHooks) {
172+
this.rootHooks(options.rootHooks);
173+
}
168174
}
169175

170176
/**
@@ -1047,3 +1053,46 @@ Mocha.prototype.run = function(fn) {
10471053

10481054
return runner.run(done);
10491055
};
1056+
1057+
/**
1058+
* Assigns hooks to the root suite
1059+
* @param {MochaRootHookObject} [hooks] - Hooks to assign to root suite
1060+
* @chainable
1061+
*/
1062+
Mocha.prototype.rootHooks = function rootHooks(hooks) {
1063+
if (utils.type(hooks) === 'object') {
1064+
var beforeAll = [].concat(hooks.beforeAll || []);
1065+
var beforeEach = [].concat(hooks.beforeEach || []);
1066+
var afterAll = [].concat(hooks.afterAll || []);
1067+
var afterEach = [].concat(hooks.afterEach || []);
1068+
var rootSuite = this.suite;
1069+
beforeAll.forEach(function(hook) {
1070+
rootSuite.beforeAll(hook);
1071+
});
1072+
beforeEach.forEach(function(hook) {
1073+
rootSuite.beforeEach(hook);
1074+
});
1075+
afterAll.forEach(function(hook) {
1076+
rootSuite.afterAll(hook);
1077+
});
1078+
afterEach.forEach(function(hook) {
1079+
rootSuite.afterEach(hook);
1080+
});
1081+
}
1082+
return this;
1083+
};
1084+
1085+
/**
1086+
* An alternative way to define root hooks that works with parallel runs.
1087+
* @typedef {Object} MochaRootHookObject
1088+
* @property {Function|Function[]} [beforeAll] - "Before all" hook(s)
1089+
* @property {Function|Function[]} [beforeEach] - "Before each" hook(s)
1090+
* @property {Function|Function[]} [afterAll] - "After all" hook(s)
1091+
* @property {Function|Function[]} [afterEach] - "After each" hook(s)
1092+
*/
1093+
1094+
/**
1095+
* An function that returns a {@link MochaRootHookObject}, either sync or async.
1096+
* @callback MochaRootHookFunction
1097+
* @returns {MochaRootHookObject|Promise<MochaRootHookObject>}
1098+
*/

lib/utils.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ exports.isString = function(obj) {
6363
exports.slug = function(str) {
6464
return str
6565
.toLowerCase()
66-
.replace(/ +/g, '-')
67-
.replace(/[^-\w]/g, '');
66+
.replace(/\s+/g, '-')
67+
.replace(/[^-\w]/g, '')
68+
.replace(/-{2,}/g, '-');
6869
};
6970

7071
/**
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
exports.mochaHooks = {
4+
beforeAll() {
5+
console.log('beforeAll');
6+
},
7+
beforeEach() {
8+
console.log('beforeEach');
9+
},
10+
afterAll() {
11+
console.log('afterAll');
12+
},
13+
afterEach() {
14+
console.log('afterEach');
15+
}
16+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
exports.mochaHooks = {
4+
beforeAll: [
5+
function() {
6+
console.log('beforeAll array 1');
7+
},
8+
function() {
9+
console.log('beforeAll array 2');
10+
}
11+
],
12+
beforeEach: [
13+
function() {
14+
console.log('beforeEach array 1');
15+
},
16+
function() {
17+
console.log('beforeEach array 2');
18+
}
19+
],
20+
afterAll: [
21+
function() {
22+
console.log('afterAll array 1');
23+
},
24+
function() {
25+
console.log('afterAll array 2');
26+
}
27+
],
28+
afterEach: [
29+
function() {
30+
console.log('afterEach array 1');
31+
},
32+
function() {
33+
console.log('afterEach array 2');
34+
}
35+
]
36+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
3+
exports.mochaHooks = async () => ({
4+
beforeAll() {
5+
console.log('beforeAll');
6+
},
7+
beforeEach() {
8+
console.log('beforeEach');
9+
},
10+
afterAll() {
11+
console.log('afterAll');
12+
},
13+
afterEach() {
14+
console.log('afterEach');
15+
}
16+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict';
2+
3+
exports.mochaHooks = async() => ({
4+
beforeAll: [
5+
function() {
6+
console.log('beforeAll array 1');
7+
},
8+
function() {
9+
console.log('beforeAll array 2');
10+
}
11+
],
12+
beforeEach: [
13+
function() {
14+
console.log('beforeEach array 1');
15+
},
16+
function() {
17+
console.log('beforeEach array 2');
18+
}
19+
],
20+
afterAll: [
21+
function() {
22+
console.log('afterAll array 1');
23+
},
24+
function() {
25+
console.log('afterAll array 2');
26+
}
27+
],
28+
afterEach: [
29+
function() {
30+
console.log('afterEach array 1');
31+
},
32+
function() {
33+
console.log('afterEach array 2');
34+
}
35+
]
36+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// run with --require root-hook-defs-a.fixture.js --require
2+
// root-hook-defs-b.fixture.js
3+
4+
it('should also have some root hooks', function() {
5+
// test
6+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// run with --require root-hook-defs-a.fixture.js --require
2+
// root-hook-defs-b.fixture.js
3+
4+
it('should have some root hooks', function() {
5+
// test
6+
});

0 commit comments

Comments
 (0)