mocha 7.1.1 → 8.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,10 +10,12 @@
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
12
  const debug = require('debug')('mocha:cli:run:helpers');
13
- const watchRun = require('./watch-run');
13
+ const {watchRun, watchParallelRun} = require('./watch-run');
14
14
  const collectFiles = require('./collect-files');
15
-
16
- const cwd = (exports.cwd = process.cwd());
15
+ const {type} = require('../utils');
16
+ const {format} = require('util');
17
+ const {createInvalidPluginError, createUnsupportedError} = require('../errors');
18
+ const {requireOrImport} = require('../esm-utils');
17
19
 
18
20
  /**
19
21
  * Exits Mocha when tests + code under test has finished execution (default)
@@ -73,20 +75,66 @@ exports.list = str =>
73
75
  Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);
74
76
 
75
77
  /**
76
- * `require()` the modules as required by `--require <require>`
78
+ * `require()` the modules as required by `--require <require>`.
79
+ *
80
+ * Returns array of `mochaHooks` exports, if any.
77
81
  * @param {string[]} requires - Modules to require
82
+ * @returns {Promise<MochaRootHookObject|MochaRootHookFunction>} Any root hooks
78
83
  * @private
79
84
  */
80
- exports.handleRequires = (requires = []) => {
81
- requires.forEach(mod => {
85
+ exports.handleRequires = async (requires = []) => {
86
+ const acc = [];
87
+ for (const mod of requires) {
82
88
  let modpath = mod;
83
- if (fs.existsSync(mod, {cwd}) || fs.existsSync(`${mod}.js`, {cwd})) {
89
+ // this is relative to cwd
90
+ if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
84
91
  modpath = path.resolve(mod);
85
- debug(`resolved ${mod} to ${modpath}`);
92
+ debug('resolved required file %s to %s', mod, modpath);
86
93
  }
87
- require(modpath);
88
- debug(`loaded require "${mod}"`);
89
- });
94
+ const requiredModule = await requireOrImport(modpath);
95
+ if (
96
+ requiredModule &&
97
+ typeof requiredModule === 'object' &&
98
+ requiredModule.mochaHooks
99
+ ) {
100
+ const mochaHooksType = type(requiredModule.mochaHooks);
101
+ if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') {
102
+ debug('found root hooks in required file %s', mod);
103
+ acc.push(requiredModule.mochaHooks);
104
+ } else {
105
+ throw createUnsupportedError(
106
+ 'mochaHooks must be an object or a function returning (or fulfilling with) an object'
107
+ );
108
+ }
109
+ }
110
+ debug('loaded required module "%s"', mod);
111
+ }
112
+ return acc;
113
+ };
114
+
115
+ /**
116
+ * Loads root hooks as exported via `mochaHooks` from required files.
117
+ * These can be sync/async functions returning objects, or just objects.
118
+ * Flattens to a single object.
119
+ * @param {Array<MochaRootHookObject|MochaRootHookFunction>} rootHooks - Array of root hooks
120
+ * @private
121
+ * @returns {MochaRootHookObject}
122
+ */
123
+ exports.loadRootHooks = async rootHooks => {
124
+ const rootHookObjects = await Promise.all(
125
+ rootHooks.map(async hook => (/function$/.test(type(hook)) ? hook() : hook))
126
+ );
127
+
128
+ return rootHookObjects.reduce(
129
+ (acc, hook) => {
130
+ acc.beforeAll = acc.beforeAll.concat(hook.beforeAll || []);
131
+ acc.beforeEach = acc.beforeEach.concat(hook.beforeEach || []);
132
+ acc.afterAll = acc.afterAll.concat(hook.afterAll || []);
133
+ acc.afterEach = acc.afterEach.concat(hook.afterEach || []);
134
+ return acc;
135
+ },
136
+ {beforeAll: [], beforeEach: [], afterAll: [], afterEach: []}
137
+ );
90
138
  };
91
139
 
92
140
  /**
@@ -101,32 +149,61 @@ exports.handleRequires = (requires = []) => {
101
149
  */
102
150
  const singleRun = async (mocha, {exit}, fileCollectParams) => {
103
151
  const files = collectFiles(fileCollectParams);
104
- debug('running tests with files', files);
152
+ debug('single run with %d file(s)', files.length);
105
153
  mocha.files = files;
106
154
 
155
+ // handles ESM modules
107
156
  await mocha.loadFilesAsync();
108
157
  return mocha.run(exit ? exitMocha : exitMochaLater);
109
158
  };
110
159
 
111
160
  /**
112
- * Actually run tests
161
+ * Collect files and run tests (using `BufferedRunner`).
162
+ *
163
+ * This is `async` for consistency.
164
+ *
113
165
  * @param {Mocha} mocha - Mocha instance
114
- * @param {Object} opts - Command line options
166
+ * @param {Options} options - Command line options
167
+ * @param {Object} fileCollectParams - Parameters that control test
168
+ * file collection. See `lib/cli/collect-files.js`.
169
+ * @returns {Promise<BufferedRunner>}
170
+ * @ignore
115
171
  * @private
116
- * @returns {Promise}
172
+ */
173
+ const parallelRun = async (mocha, options, fileCollectParams) => {
174
+ const files = collectFiles(fileCollectParams);
175
+ debug(
176
+ 'executing %d test file(s) across %d concurrent jobs',
177
+ files.length,
178
+ options.jobs
179
+ );
180
+ mocha.files = files;
181
+
182
+ // note that we DO NOT load any files here; this is handled by the worker
183
+ return mocha.run(options.exit ? exitMocha : exitMochaLater);
184
+ };
185
+
186
+ /**
187
+ * Actually run tests. Delegates to one of four different functions:
188
+ * - `singleRun`: run tests in serial & exit
189
+ * - `watchRun`: run tests in serial, rerunning as files change
190
+ * - `parallelRun`: run tests in parallel & exit
191
+ * - `watchParallelRun`: run tests in parallel, rerunning as files change
192
+ * @param {Mocha} mocha - Mocha instance
193
+ * @param {Options} opts - Command line options
194
+ * @private
195
+ * @returns {Promise<Runner>}
117
196
  */
118
197
  exports.runMocha = async (mocha, options) => {
119
198
  const {
120
199
  watch = false,
121
200
  extension = [],
122
- exit = false,
123
201
  ignore = [],
124
202
  file = [],
203
+ parallel = false,
125
204
  recursive = false,
126
205
  sort = false,
127
- spec = [],
128
- watchFiles,
129
- watchIgnore
206
+ spec = []
130
207
  } = options;
131
208
 
132
209
  const fileCollectParams = {
@@ -138,43 +215,63 @@ exports.runMocha = async (mocha, options) => {
138
215
  spec
139
216
  };
140
217
 
218
+ let run;
141
219
  if (watch) {
142
- watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
220
+ run = parallel ? watchParallelRun : watchRun;
143
221
  } else {
144
- await singleRun(mocha, {exit}, fileCollectParams);
222
+ run = parallel ? parallelRun : singleRun;
145
223
  }
224
+
225
+ return run(mocha, options, fileCollectParams);
146
226
  };
147
227
 
148
228
  /**
149
- * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts
150
- * that it actually exists.
151
- * @todo XXX This must get run after requires are processed, as it'll prevent
152
- * interfaces from loading.
229
+ * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts that
230
+ * it actually exists. This must be run _after_ requires are processed (see
231
+ * {@link handleRequires}), as it'll prevent interfaces from loading otherwise.
153
232
  * @param {Object} opts - Options object
154
- * @param {string} key - Resolvable module name or path
155
- * @param {Object} [map] - An object perhaps having key `key`
233
+ * @param {"reporter"|"interface"} pluginType - Type of plugin.
234
+ * @param {Object} [map] - An object perhaps having key `key`. Used as a cache
235
+ * of sorts; `Mocha.reporters` is one, where each key corresponds to a reporter
236
+ * name
156
237
  * @private
157
238
  */
158
- exports.validatePlugin = (opts, key, map = {}) => {
159
- if (Array.isArray(opts[key])) {
160
- throw new TypeError(`"--${key} <${key}>" can only be specified once`);
239
+ exports.validatePlugin = (opts, pluginType, map = {}) => {
240
+ /**
241
+ * This should be a unique identifier; either a string (present in `map`),
242
+ * or a resolvable (via `require.resolve`) module ID/path.
243
+ * @type {string}
244
+ */
245
+ const pluginId = opts[pluginType];
246
+
247
+ if (Array.isArray(pluginId)) {
248
+ throw createInvalidPluginError(
249
+ `"--${pluginType}" can only be specified once`,
250
+ pluginType
251
+ );
161
252
  }
162
253
 
163
- const unknownError = () => new Error(`Unknown "${key}": ${opts[key]}`);
254
+ const unknownError = err =>
255
+ createInvalidPluginError(
256
+ format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err),
257
+ pluginType,
258
+ pluginId
259
+ );
164
260
 
165
- if (!map[opts[key]]) {
261
+ // if this exists, then it's already loaded, so nothing more to do.
262
+ if (!map[pluginId]) {
166
263
  try {
167
- opts[key] = require(opts[key]);
264
+ opts[pluginType] = require(pluginId);
168
265
  } catch (err) {
169
266
  if (err.code === 'MODULE_NOT_FOUND') {
170
267
  // Try to load reporters from a path (absolute or relative)
171
268
  try {
172
- opts[key] = require(path.resolve(process.cwd(), opts[key]));
269
+ opts[pluginType] = require(path.resolve(pluginId));
173
270
  } catch (err) {
174
- throw unknownError();
271
+ throw unknownError(err);
175
272
  }
176
273
  } else {
177
- throw unknownError();
274
+ throw unknownError(err);
178
275
  }
179
276
  }
180
277
  }
@@ -42,16 +42,16 @@ exports.types = {
42
42
  'list-interfaces',
43
43
  'list-reporters',
44
44
  'no-colors',
45
+ 'parallel',
45
46
  'recursive',
46
47
  'sort',
47
48
  'watch'
48
49
  ],
49
- number: ['retries'],
50
+ number: ['retries', 'jobs'],
50
51
  string: [
51
52
  'config',
52
53
  'fgrep',
53
54
  'grep',
54
- 'opts',
55
55
  'package',
56
56
  'reporter',
57
57
  'ui',
@@ -76,7 +76,9 @@ exports.aliases = {
76
76
  growl: ['G'],
77
77
  ignore: ['exclude'],
78
78
  invert: ['i'],
79
+ jobs: ['j'],
79
80
  'no-colors': ['C'],
81
+ parallel: ['p'],
80
82
  reporter: ['R'],
81
83
  'reporter-option': ['reporter-options', 'O'],
82
84
  require: ['r'],
package/lib/cli/run.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * @private
8
8
  */
9
9
 
10
+ const symbols = require('log-symbols');
11
+ const ansi = require('ansi-colors');
10
12
  const Mocha = require('../mocha');
11
13
  const {
12
14
  createUnsupportedError,
@@ -18,6 +20,7 @@ const {
18
20
  list,
19
21
  handleRequires,
20
22
  validatePlugin,
23
+ loadRootHooks,
21
24
  runMocha
22
25
  } = require('./run-helpers');
23
26
  const {ONE_AND_DONES, ONE_AND_DONE_ARGS} = require('./one-and-dones');
@@ -150,6 +153,13 @@ exports.builder = yargs =>
150
153
  description: 'Inverts --grep and --fgrep matches',
151
154
  group: GROUPS.FILTERS
152
155
  },
156
+ jobs: {
157
+ description:
158
+ 'Number of concurrent jobs for --parallel; use 1 to run in serial',
159
+ defaultDescription: '(number of CPU cores - 1)',
160
+ requiresArg: true,
161
+ group: GROUPS.RULES
162
+ },
153
163
  'list-interfaces': {
154
164
  conflicts: Array.from(ONE_AND_DONE_ARGS),
155
165
  description: 'List built-in user interfaces & exit'
@@ -163,19 +173,16 @@ exports.builder = yargs =>
163
173
  group: GROUPS.OUTPUT,
164
174
  hidden: true
165
175
  },
166
- opts: {
167
- default: defaults.opts,
168
- description: 'Path to `mocha.opts` (DEPRECATED)',
169
- group: GROUPS.CONFIG,
170
- normalize: true,
171
- requiresArg: true
172
- },
173
176
  package: {
174
177
  description: 'Path to package.json for config',
175
178
  group: GROUPS.CONFIG,
176
179
  normalize: true,
177
180
  requiresArg: true
178
181
  },
182
+ parallel: {
183
+ description: 'Run tests in parallel',
184
+ group: GROUPS.RULES
185
+ },
179
186
  recursive: {
180
187
  description: 'Look for tests in subdirectories',
181
188
  group: GROUPS.FILES
@@ -278,6 +285,40 @@ exports.builder = yargs =>
278
285
  );
279
286
  }
280
287
 
288
+ if (argv.parallel) {
289
+ // yargs.conflicts() can't deal with `--file foo.js --no-parallel`, either
290
+ if (argv.file) {
291
+ throw createUnsupportedError(
292
+ '--parallel runs test files in a non-deterministic order, and is mutually exclusive with --file'
293
+ );
294
+ }
295
+
296
+ // or this
297
+ if (argv.sort) {
298
+ throw createUnsupportedError(
299
+ '--parallel runs test files in a non-deterministic order, and is mutually exclusive with --sort'
300
+ );
301
+ }
302
+
303
+ if (argv.reporter === 'progress') {
304
+ throw createUnsupportedError(
305
+ '--reporter=progress is mutually exclusive with --parallel'
306
+ );
307
+ }
308
+
309
+ if (argv.reporter === 'markdown') {
310
+ throw createUnsupportedError(
311
+ '--reporter=markdown is mutually exclusive with --parallel'
312
+ );
313
+ }
314
+
315
+ if (argv.reporter === 'json-stream') {
316
+ throw createUnsupportedError(
317
+ '--reporter=json-stream is mutually exclusive with --parallel'
318
+ );
319
+ }
320
+ }
321
+
281
322
  if (argv.compilers) {
282
323
  throw createUnsupportedError(
283
324
  `--compilers is DEPRECATED and no longer supported.
@@ -285,13 +326,32 @@ exports.builder = yargs =>
285
326
  );
286
327
  }
287
328
 
288
- // load requires first, because it can impact "plugin" validation
289
- handleRequires(argv.require);
290
- validatePlugin(argv, 'reporter', Mocha.reporters);
291
- validatePlugin(argv, 'ui', Mocha.interfaces);
329
+ if (argv.opts) {
330
+ throw createUnsupportedError(
331
+ `--opts: configuring Mocha via 'mocha.opts' is DEPRECATED and no longer supported.
332
+ Please use a configuration file instead.`
333
+ );
334
+ }
292
335
 
293
336
  return true;
294
337
  })
338
+ .middleware(async (argv, yargs) => {
339
+ // currently a failing middleware does not work nicely with yargs' `fail()`.
340
+ try {
341
+ // load requires first, because it can impact "plugin" validation
342
+ const rawRootHooks = await handleRequires(argv.require);
343
+ validatePlugin(argv, 'reporter', Mocha.reporters);
344
+ validatePlugin(argv, 'ui', Mocha.interfaces);
345
+
346
+ if (rawRootHooks && rawRootHooks.length) {
347
+ argv.rootHooks = await loadRootHooks(rawRootHooks);
348
+ }
349
+ } catch (err) {
350
+ // this could be a bad --require, bad reporter, ui, etc.
351
+ console.error(`\n${symbols.error} ${ansi.red('ERROR:')}`, err);
352
+ yargs.exit(1);
353
+ }
354
+ })
295
355
  .array(types.array)
296
356
  .boolean(types.boolean)
297
357
  .string(types.string)