mocha 7.2.0 → 8.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/bin/mocha +17 -2
  3. package/browser-entry.js +26 -9
  4. package/lib/browser/growl.js +2 -1
  5. package/lib/browser/highlight-tags.js +39 -0
  6. package/lib/browser/parse-query.js +24 -0
  7. package/lib/browser/progress.js +4 -0
  8. package/lib/browser/template.html +7 -5
  9. package/lib/cli/cli.js +2 -2
  10. package/lib/cli/collect-files.js +15 -9
  11. package/lib/cli/config.js +0 -1
  12. package/lib/cli/init.js +1 -2
  13. package/lib/cli/lookup-files.js +145 -0
  14. package/lib/cli/node-flags.js +2 -2
  15. package/lib/cli/options.js +11 -87
  16. package/lib/cli/run-helpers.js +54 -16
  17. package/lib/cli/run-option-metadata.js +4 -2
  18. package/lib/cli/run.js +61 -14
  19. package/lib/cli/watch-run.js +211 -51
  20. package/lib/context.js +0 -15
  21. package/lib/errors.js +26 -3
  22. package/lib/esm-utils.js +11 -6
  23. package/lib/hook.js +24 -0
  24. package/lib/interfaces/common.js +19 -11
  25. package/lib/mocha.js +137 -121
  26. package/lib/mocharc.json +0 -1
  27. package/lib/nodejs/buffered-worker-pool.js +174 -0
  28. package/lib/{growl.js → nodejs/growl.js} +3 -2
  29. package/lib/nodejs/parallel-buffered-runner.js +295 -0
  30. package/lib/nodejs/reporters/parallel-buffered.js +133 -0
  31. package/lib/nodejs/serializer.js +404 -0
  32. package/lib/nodejs/worker.js +154 -0
  33. package/lib/pending.js +4 -0
  34. package/lib/reporters/base.js +25 -12
  35. package/lib/reporters/landing.js +3 -3
  36. package/lib/reporters/tap.js +1 -2
  37. package/lib/reporters/xunit.js +3 -2
  38. package/lib/runnable.js +18 -30
  39. package/lib/runner.js +58 -64
  40. package/lib/suite.js +32 -24
  41. package/lib/test.js +28 -1
  42. package/lib/utils.js +19 -206
  43. package/mocha.js +25522 -18248
  44. package/mocha.js.map +1 -0
  45. package/package.json +52 -42
  46. package/lib/browser/tty.js +0 -13
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Main entry point for handling filesystem-based configuration,
5
- * whether that's `mocha.opts` or a config file or `package.json` or whatever.
5
+ * whether that's a config file or `package.json` or whatever.
6
6
  * @module
7
7
  */
8
8
 
@@ -15,7 +15,6 @@ const mocharc = require('../mocharc.json');
15
15
  const {list} = require('./run-helpers');
16
16
  const {loadConfig, findConfig} = require('./config');
17
17
  const findUp = require('find-up');
18
- const {deprecate} = require('../utils');
19
18
  const debug = require('debug')('mocha:cli:options');
20
19
  const {isNodeFlag} = require('./node-flags');
21
20
 
@@ -55,16 +54,19 @@ const configuration = Object.assign({}, YARGS_PARSER_CONFIG, {
55
54
 
56
55
  /**
57
56
  * This is a really fancy way to:
58
- * - ensure unique values for `array`-type options
59
- * - use its array's last element for `boolean`/`number`/`string`- options given multiple times
57
+ * - `array`-type options: ensure unique values and evtl. split comma-delimited lists
58
+ * - `boolean`/`number`/`string`- options: use last element when given multiple times
60
59
  * This is passed as the `coerce` option to `yargs-parser`
61
60
  * @private
62
61
  * @ignore
63
62
  */
63
+ const globOptions = ['spec', 'ignore'];
64
64
  const coerceOpts = Object.assign(
65
65
  types.array.reduce(
66
66
  (acc, arg) =>
67
- Object.assign(acc, {[arg]: v => Array.from(new Set(list(v)))}),
67
+ Object.assign(acc, {
68
+ [arg]: v => Array.from(new Set(globOptions.includes(arg) ? v : list(v)))
69
+ }),
68
70
  {}
69
71
  ),
70
72
  types.boolean
@@ -143,71 +145,6 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => {
143
145
  return result.argv;
144
146
  };
145
147
 
146
- /**
147
- * - Replaces comments with empty strings
148
- * - Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space
149
- * - Splits on whitespace, creating array of substrings
150
- * - Filters empty string elements from array
151
- * - Replaces any HTML space with space
152
- * @summary Parses options read from run-control file.
153
- * @private
154
- * @param {string} content - Content read from run-control file.
155
- * @returns {string[]} cmdline options (and associated arguments)
156
- * @ignore
157
- */
158
- const parseMochaOpts = content =>
159
- content
160
- .replace(/^#.*$/gm, '')
161
- .replace(/\\\s/g, '%20')
162
- .split(/\s/)
163
- .filter(Boolean)
164
- .map(value => value.replace(/%20/g, ' '));
165
-
166
- /**
167
- * Given filepath in `args.opts`, attempt to load and parse a `mocha.opts` file.
168
- * @param {Object} [args] - Arguments object
169
- * @param {string|boolean} [args.opts] - Filepath to mocha.opts; defaults to whatever's in `mocharc.opts`, or `false` to skip
170
- * @returns {external:yargsParser.Arguments|void} If read, object containing parsed arguments
171
- * @memberof module:lib/cli/options
172
- * @see {@link /#mochaopts|mocha.opts}
173
- * @public
174
- */
175
- const loadMochaOpts = (args = {}) => {
176
- let result;
177
- let filepath = args.opts;
178
- // /dev/null is backwards compat
179
- if (filepath === false || filepath === '/dev/null') {
180
- return result;
181
- }
182
- filepath = filepath || mocharc.opts;
183
- result = {};
184
- let mochaOpts;
185
- try {
186
- mochaOpts = fs.readFileSync(filepath, 'utf8');
187
- debug(`read ${filepath}`);
188
- } catch (err) {
189
- if (args.opts) {
190
- throw new Error(`Unable to read ${filepath}: ${err}`);
191
- }
192
- // ignore otherwise. we tried
193
- debug(`No mocha.opts found at ${filepath}`);
194
- }
195
-
196
- // real args should override `mocha.opts` which should override defaults.
197
- // if there's an exception to catch here, I'm not sure what it is.
198
- // by attaching the `no-opts` arg, we avoid re-parsing of `mocha.opts`.
199
- if (mochaOpts) {
200
- deprecate(
201
- 'Configuration via mocha.opts is DEPRECATED and will be removed from a future version of Mocha. Use RC files or package.json instead.'
202
- );
203
- result = parse(parseMochaOpts(mochaOpts));
204
- debug(`${filepath} parsed succesfully`);
205
- }
206
- return result;
207
- };
208
-
209
- module.exports.loadMochaOpts = loadMochaOpts;
210
-
211
148
  /**
212
149
  * Given path to config file in `args.config`, attempt to load & parse config file.
213
150
  * @param {Object} [args] - Arguments object
@@ -267,11 +204,10 @@ module.exports.loadPkgRc = loadPkgRc;
267
204
  * 1. Command-line args
268
205
  * 2. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
269
206
  * 3. `mocha` prop of `package.json`
270
- * 4. `mocha.opts`
271
- * 5. default configuration (`lib/mocharc.json`)
207
+ * 4. default configuration (`lib/mocharc.json`)
272
208
  *
273
209
  * If a {@link module:lib/cli/one-and-dones.ONE_AND_DONE_ARGS "one-and-done" option} is present in the `argv` array, no external config files will be read.
274
- * @summary Parses options read from `mocha.opts`, `.mocharc.*` and `package.json`.
210
+ * @summary Parses options read from `.mocharc.*` and `package.json`.
275
211
  * @param {string|string[]} [argv] - Arguments to parse
276
212
  * @public
277
213
  * @memberof module:lib/cli/options
@@ -279,7 +215,7 @@ module.exports.loadPkgRc = loadPkgRc;
279
215
  */
280
216
  const loadOptions = (argv = []) => {
281
217
  let args = parse(argv);
282
- // short-circuit: look for a flag that would abort loading of mocha.opts
218
+ // short-circuit: look for a flag that would abort loading of options
283
219
  if (
284
220
  Array.from(ONE_AND_DONE_ARGS).reduce(
285
221
  (acc, arg) => acc || arg in args,
@@ -291,7 +227,6 @@ const loadOptions = (argv = []) => {
291
227
 
292
228
  const rcConfig = loadRc(args);
293
229
  const pkgConfig = loadPkgRc(args);
294
- const optsConfig = loadMochaOpts(args);
295
230
 
296
231
  if (rcConfig) {
297
232
  args.config = false;
@@ -301,19 +236,8 @@ const loadOptions = (argv = []) => {
301
236
  args.package = false;
302
237
  args._ = args._.concat(pkgConfig._ || []);
303
238
  }
304
- if (optsConfig) {
305
- args.opts = false;
306
- args._ = args._.concat(optsConfig._ || []);
307
- }
308
239
 
309
- args = parse(
310
- args._,
311
- mocharc,
312
- args,
313
- rcConfig || {},
314
- pkgConfig || {},
315
- optsConfig || {}
316
- );
240
+ args = parse(args._, mocharc, args, rcConfig || {}, pkgConfig || {});
317
241
 
318
242
  // recombine positional arguments and "spec"
319
243
  if (args.spec) {
@@ -10,11 +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
15
  const {type} = require('../utils');
16
16
  const {format} = require('util');
17
17
  const {createInvalidPluginError, createUnsupportedError} = require('../errors');
18
+ const {requireOrImport} = require('../esm-utils');
18
19
 
19
20
  /**
20
21
  * Exits Mocha when tests + code under test has finished execution (default)
@@ -81,16 +82,21 @@ exports.list = str =>
81
82
  * @returns {Promise<MochaRootHookObject|MochaRootHookFunction>} Any root hooks
82
83
  * @private
83
84
  */
84
- exports.handleRequires = async (requires = []) =>
85
- requires.reduce((acc, mod) => {
85
+ exports.handleRequires = async (requires = []) => {
86
+ const acc = [];
87
+ for (const mod of requires) {
86
88
  let modpath = mod;
87
89
  // this is relative to cwd
88
90
  if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
89
91
  modpath = path.resolve(mod);
90
92
  debug('resolved required file %s to %s', mod, modpath);
91
93
  }
92
- const requiredModule = require(modpath);
93
- if (type(requiredModule) === 'object' && requiredModule.mochaHooks) {
94
+ const requiredModule = await requireOrImport(modpath);
95
+ if (
96
+ requiredModule &&
97
+ typeof requiredModule === 'object' &&
98
+ requiredModule.mochaHooks
99
+ ) {
94
100
  const mochaHooksType = type(requiredModule.mochaHooks);
95
101
  if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') {
96
102
  debug('found root hooks in required file %s', mod);
@@ -102,8 +108,9 @@ exports.handleRequires = async (requires = []) =>
102
108
  }
103
109
  }
104
110
  debug('loaded required module "%s"', mod);
105
- return acc;
106
- }, []);
111
+ }
112
+ return acc;
113
+ };
107
114
 
108
115
  /**
109
116
  * Loads root hooks as exported via `mochaHooks` from required files.
@@ -151,24 +158,52 @@ const singleRun = async (mocha, {exit}, fileCollectParams) => {
151
158
  };
152
159
 
153
160
  /**
154
- * Actually run tests
161
+ * Collect files and run tests (using `BufferedRunner`).
162
+ *
163
+ * This is `async` for consistency.
164
+ *
155
165
  * @param {Mocha} mocha - Mocha instance
156
- * @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
157
171
  * @private
158
- * @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>}
159
196
  */
160
197
  exports.runMocha = async (mocha, options) => {
161
198
  const {
162
199
  watch = false,
163
200
  extension = [],
164
- exit = false,
165
201
  ignore = [],
166
202
  file = [],
203
+ parallel = false,
167
204
  recursive = false,
168
205
  sort = false,
169
- spec = [],
170
- watchFiles,
171
- watchIgnore
206
+ spec = []
172
207
  } = options;
173
208
 
174
209
  const fileCollectParams = {
@@ -180,11 +215,14 @@ exports.runMocha = async (mocha, options) => {
180
215
  spec
181
216
  };
182
217
 
218
+ let run;
183
219
  if (watch) {
184
- watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
220
+ run = parallel ? watchParallelRun : watchRun;
185
221
  } else {
186
- await singleRun(mocha, {exit}, fileCollectParams);
222
+ run = parallel ? parallelRun : singleRun;
187
223
  }
224
+
225
+ return run(mocha, options, fileCollectParams);
188
226
  };
189
227
 
190
228
  /**
@@ -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,
@@ -151,6 +153,13 @@ exports.builder = yargs =>
151
153
  description: 'Inverts --grep and --fgrep matches',
152
154
  group: GROUPS.FILTERS
153
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
+ },
154
163
  'list-interfaces': {
155
164
  conflicts: Array.from(ONE_AND_DONE_ARGS),
156
165
  description: 'List built-in user interfaces & exit'
@@ -164,19 +173,16 @@ exports.builder = yargs =>
164
173
  group: GROUPS.OUTPUT,
165
174
  hidden: true
166
175
  },
167
- opts: {
168
- default: defaults.opts,
169
- description: 'Path to `mocha.opts` (DEPRECATED)',
170
- group: GROUPS.CONFIG,
171
- normalize: true,
172
- requiresArg: true
173
- },
174
176
  package: {
175
177
  description: 'Path to package.json for config',
176
178
  group: GROUPS.CONFIG,
177
179
  normalize: true,
178
180
  requiresArg: true
179
181
  },
182
+ parallel: {
183
+ description: 'Run tests in parallel',
184
+ group: GROUPS.RULES
185
+ },
180
186
  recursive: {
181
187
  description: 'Look for tests in subdirectories',
182
188
  group: GROUPS.FILES
@@ -279,6 +285,40 @@ exports.builder = yargs =>
279
285
  );
280
286
  }
281
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
+
282
322
  if (argv.compilers) {
283
323
  throw createUnsupportedError(
284
324
  `--compilers is DEPRECATED and no longer supported.
@@ -295,14 +335,21 @@ exports.builder = yargs =>
295
335
 
296
336
  return true;
297
337
  })
298
- .middleware(async argv => {
299
- // load requires first, because it can impact "plugin" validation
300
- const rawRootHooks = await handleRequires(argv.require);
301
- validatePlugin(argv, 'reporter', Mocha.reporters);
302
- validatePlugin(argv, 'ui', Mocha.interfaces);
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);
303
345
 
304
- if (rawRootHooks.length) {
305
- argv.rootHooks = await loadRootHooks(rawRootHooks);
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);
306
353
  }
307
354
  })
308
355
  .array(types.array)