mocha 8.1.1 → 8.2.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.
@@ -6,13 +6,31 @@
6
6
 
7
7
  'use strict';
8
8
 
9
- const allSettled = require('promise.allsettled');
9
+ const allSettled = require('@ungap/promise-all-settled').bind(Promise);
10
10
  const Runner = require('../runner');
11
11
  const {EVENT_RUN_BEGIN, EVENT_RUN_END} = Runner.constants;
12
12
  const debug = require('debug')('mocha:parallel:parallel-buffered-runner');
13
13
  const {BufferedWorkerPool} = require('./buffered-worker-pool');
14
14
  const {setInterval, clearInterval} = global;
15
- const {createMap} = require('../utils');
15
+ const {createMap, constants} = require('../utils');
16
+ const {MOCHA_ID_PROP_NAME} = constants;
17
+ const {createFatalError} = require('../errors');
18
+
19
+ const DEFAULT_WORKER_REPORTER = require.resolve(
20
+ './reporters/parallel-buffered'
21
+ );
22
+
23
+ /**
24
+ * List of options to _not_ serialize for transmission to workers
25
+ */
26
+ const DENY_OPTIONS = [
27
+ 'globalSetup',
28
+ 'globalTeardown',
29
+ 'parallel',
30
+ 'p',
31
+ 'jobs',
32
+ 'j'
33
+ ];
16
34
 
17
35
  /**
18
36
  * Outputs a debug statement with worker stats
@@ -56,7 +74,7 @@ const states = createMap({
56
74
  /**
57
75
  * This `Runner` delegates tests runs to worker threads. Does not execute any
58
76
  * {@link Runnable}s by itself!
59
- * @private
77
+ * @public
60
78
  */
61
79
  class ParallelBufferedRunner extends Runner {
62
80
  constructor(...args) {
@@ -76,6 +94,10 @@ class ParallelBufferedRunner extends Runner {
76
94
  }
77
95
  });
78
96
 
97
+ this._workerReporter = DEFAULT_WORKER_REPORTER;
98
+ this._linkPartialObjects = false;
99
+ this._linkedObjectMap = new Map();
100
+
79
101
  this.once(Runner.constants.EVENT_RUN_END, () => {
80
102
  this._state = COMPLETE;
81
103
  });
@@ -86,12 +108,68 @@ class ParallelBufferedRunner extends Runner {
86
108
  * @param {BufferedWorkerPool} pool - Worker pool
87
109
  * @param {Options} options - Mocha options
88
110
  * @returns {FileRunner} Mapping function
111
+ * @private
89
112
  */
90
113
  _createFileRunner(pool, options) {
114
+ /**
115
+ * Emits event and sets `BAILING` state, if necessary.
116
+ * @param {Object} event - Event having `eventName`, maybe `data` and maybe `error`
117
+ * @param {number} failureCount - Failure count
118
+ */
119
+ const emitEvent = (event, failureCount) => {
120
+ this.emit(event.eventName, event.data, event.error);
121
+ if (
122
+ this._state !== BAILING &&
123
+ event.data &&
124
+ event.data._bail &&
125
+ (failureCount || event.error)
126
+ ) {
127
+ debug('run(): nonzero failure count & found bail flag');
128
+ // we need to let the events complete for this file, as the worker
129
+ // should run any cleanup hooks
130
+ this._state = BAILING;
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Given an event, recursively find any objects in its data that have ID's, and create object references to already-seen objects.
136
+ * @param {Object} event - Event having `eventName`, maybe `data` and maybe `error`
137
+ */
138
+ const linkEvent = event => {
139
+ const stack = [{parent: event, prop: 'data'}];
140
+ while (stack.length) {
141
+ const {parent, prop} = stack.pop();
142
+ const obj = parent[prop];
143
+ let newObj;
144
+ if (obj && typeof obj === 'object') {
145
+ if (obj[MOCHA_ID_PROP_NAME]) {
146
+ const id = obj[MOCHA_ID_PROP_NAME];
147
+ newObj = this._linkedObjectMap.has(id)
148
+ ? Object.assign(this._linkedObjectMap.get(id), obj)
149
+ : obj;
150
+ this._linkedObjectMap.set(id, newObj);
151
+ parent[prop] = newObj;
152
+ } else {
153
+ throw createFatalError(
154
+ 'Object missing ID received in event data',
155
+ obj
156
+ );
157
+ }
158
+ }
159
+ Object.keys(newObj).forEach(key => {
160
+ const value = obj[key];
161
+ if (value && typeof value === 'object' && value[MOCHA_ID_PROP_NAME]) {
162
+ stack.push({obj: value, parent: newObj, prop: key});
163
+ }
164
+ });
165
+ }
166
+ };
167
+
91
168
  return async file => {
92
169
  debug('run(): enqueueing test file %s', file);
93
170
  try {
94
171
  const {failureCount, events} = await pool.run(file, options);
172
+
95
173
  if (this._state === BAILED) {
96
174
  // short-circuit after a graceful bail. if this happens,
97
175
  // some other worker has bailed.
@@ -107,20 +185,18 @@ class ParallelBufferedRunner extends Runner {
107
185
  );
108
186
  this.failures += failureCount; // can this ever be non-numeric?
109
187
  let event = events.shift();
110
- while (event) {
111
- this.emit(event.eventName, event.data, event.error);
112
- if (
113
- this._state !== BAILING &&
114
- event.data &&
115
- event.data._bail &&
116
- (failureCount || event.error)
117
- ) {
118
- debug('run(): nonzero failure count & found bail flag');
119
- // we need to let the events complete for this file, as the worker
120
- // should run any cleanup hooks
121
- this._state = BAILING;
188
+
189
+ if (this._linkPartialObjects) {
190
+ while (event) {
191
+ linkEvent(event);
192
+ emitEvent(event, failureCount);
193
+ event = events.shift();
194
+ }
195
+ } else {
196
+ while (event) {
197
+ emitEvent(event, failureCount);
198
+ event = events.shift();
122
199
  }
123
- event = events.shift();
124
200
  }
125
201
  if (this._state === BAILING) {
126
202
  debug('run(): terminating pool due to "bail" flag');
@@ -154,6 +230,7 @@ class ParallelBufferedRunner extends Runner {
154
230
  * Returns the listener for later call to `process.removeListener()`.
155
231
  * @param {BufferedWorkerPool} pool - Worker pool
156
232
  * @returns {SigIntListener} Listener
233
+ * @private
157
234
  */
158
235
  _bindSigIntListener(pool) {
159
236
  const sigIntListener = async () => {
@@ -197,15 +274,19 @@ class ParallelBufferedRunner extends Runner {
197
274
  * @param {{files: string[], options: Options}} opts - Files to run and
198
275
  * command-line options, respectively.
199
276
  */
200
- run(callback, {files, options} = {}) {
277
+ run(callback, {files, options = {}} = {}) {
201
278
  /**
202
279
  * Listener on `Process.SIGINT` which tries to cleanly terminate the worker pool.
203
280
  */
204
281
  let sigIntListener;
282
+
283
+ // assign the reporter the worker will use, which will be different than the
284
+ // main process' reporter
285
+ options = {...options, reporter: this._workerReporter};
286
+
205
287
  // This function should _not_ return a `Promise`; its parent (`Runner#run`)
206
288
  // returns this instance, so this should do the same. However, we want to make
207
289
  // use of `async`/`await`, so we use this IIFE.
208
-
209
290
  (async () => {
210
291
  /**
211
292
  * This is an interval that outputs stats about the worker pool every so often
@@ -235,6 +316,11 @@ class ParallelBufferedRunner extends Runner {
235
316
 
236
317
  this.emit(EVENT_RUN_BEGIN);
237
318
 
319
+ options = {...options};
320
+ DENY_OPTIONS.forEach(opt => {
321
+ delete options[opt];
322
+ });
323
+
238
324
  const results = await allSettled(
239
325
  files.map(this._createFileRunner(pool, options))
240
326
  );
@@ -257,6 +343,7 @@ class ParallelBufferedRunner extends Runner {
257
343
  if (this._state === ABORTING) {
258
344
  return;
259
345
  }
346
+
260
347
  this.emit(EVENT_RUN_END);
261
348
  debug('run(): completing with failure count %d', this.failures);
262
349
  callback(this.failures);
@@ -275,6 +362,57 @@ class ParallelBufferedRunner extends Runner {
275
362
  })();
276
363
  return this;
277
364
  }
365
+
366
+ /**
367
+ * Toggle partial object linking behavior; used for building object references from
368
+ * unique ID's.
369
+ * @param {boolean} [value] - If `true`, enable partial object linking, otherwise disable
370
+ * @returns {Runner}
371
+ * @chainable
372
+ * @public
373
+ * @example
374
+ * // this reporter needs proper object references when run in parallel mode
375
+ * class MyReporter() {
376
+ * constructor(runner) {
377
+ * this.runner.linkPartialObjects(true)
378
+ * .on(EVENT_SUITE_BEGIN, suite => {
379
+ // this Suite may be the same object...
380
+ * })
381
+ * .on(EVENT_TEST_BEGIN, test => {
382
+ * // ...as the `test.parent` property
383
+ * });
384
+ * }
385
+ * }
386
+ */
387
+ linkPartialObjects(value) {
388
+ this._linkPartialObjects = Boolean(value);
389
+ return super.linkPartialObjects(value);
390
+ }
391
+
392
+ /**
393
+ * If this class is the `Runner` in use, then this is going to return `true`.
394
+ *
395
+ * For use by reporters.
396
+ * @returns {true}
397
+ * @public
398
+ */
399
+ isParallelMode() {
400
+ return true;
401
+ }
402
+
403
+ /**
404
+ * Configures an alternate reporter for worker processes to use. Subclasses
405
+ * using worker processes should implement this.
406
+ * @public
407
+ * @param {string} path - Absolute path to alternate reporter for worker processes to use
408
+ * @returns {Runner}
409
+ * @throws When in serial mode
410
+ * @chainable
411
+ */
412
+ workerReporter(reporter) {
413
+ this._workerReporter = reporter;
414
+ return this;
415
+ }
278
416
  }
279
417
 
280
418
  module.exports = ParallelBufferedRunner;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * "Buffered" reporter used internally by a worker process when running in parallel mode.
3
- * @module reporters/parallel-buffered
4
- * @private
3
+ * @module nodejs/reporters/parallel-buffered
4
+ * @public
5
5
  */
6
6
 
7
7
  'use strict';
@@ -53,15 +53,16 @@ const EVENT_NAMES = [
53
53
  const ONCE_EVENT_NAMES = [EVENT_DELAY_BEGIN, EVENT_DELAY_END];
54
54
 
55
55
  /**
56
- * The `ParallelBuffered` reporter is for use by concurrent runs. Instead of outputting
57
- * to `STDOUT`, etc., it retains a list of events it receives and hands these
58
- * off to the callback passed into {@link Mocha#run}. That callback will then
59
- * return the data to the main process.
60
- * @private
56
+ * The `ParallelBuffered` reporter is used by each worker process in "parallel"
57
+ * mode, by default. Instead of reporting to to `STDOUT`, etc., it retains a
58
+ * list of events it receives and hands these off to the callback passed into
59
+ * {@link Mocha#run}. That callback will then return the data to the main
60
+ * process.
61
+ * @public
61
62
  */
62
63
  class ParallelBuffered extends Base {
63
64
  /**
64
- * Listens for {@link Runner} events and retains them in an `events` instance prop.
65
+ * Calls {@link ParallelBuffered#createListeners}
65
66
  * @param {Runner} runner
66
67
  */
67
68
  constructor(runner, opts) {
@@ -70,50 +71,81 @@ class ParallelBuffered extends Base {
70
71
  /**
71
72
  * Retained list of events emitted from the {@link Runner} instance.
72
73
  * @type {BufferedEvent[]}
73
- * @memberOf Buffered
74
+ * @public
74
75
  */
75
- const events = (this.events = []);
76
+ this.events = [];
76
77
 
77
78
  /**
78
- * mapping of event names to listener functions we've created,
79
- * so we can cleanly _remove_ them from the runner once it's completed.
79
+ * Map of `Runner` event names to listeners (for later teardown)
80
+ * @public
81
+ * @type {Map<string,EventListener>}
80
82
  */
81
- const listeners = new Map();
83
+ this.listeners = new Map();
82
84
 
83
- /**
84
- * Creates a listener for event `eventName` and adds it to the `listeners`
85
- * map. This is a defensive measure, so that we don't a) leak memory or b)
86
- * remove _other_ listeners that may not be associated with this reporter.
87
- * @param {string} eventName - Event name
88
- */
89
- const createListener = eventName =>
90
- listeners
91
- .set(eventName, (runnable, err) => {
92
- events.push(SerializableEvent.create(eventName, runnable, err));
93
- })
94
- .get(eventName);
85
+ this.createListeners(runner);
86
+ }
87
+
88
+ /**
89
+ * Returns a new listener which saves event data in memory to
90
+ * {@link ParallelBuffered#events}. Listeners are indexed by `eventName` and stored
91
+ * in {@link ParallelBuffered#listeners}. This is a defensive measure, so that we
92
+ * don't a) leak memory or b) remove _other_ listeners that may not be
93
+ * associated with this reporter.
94
+ *
95
+ * Subclasses could override this behavior.
96
+ *
97
+ * @public
98
+ * @param {string} eventName - Name of event to create listener for
99
+ * @returns {EventListener}
100
+ */
101
+ createListener(eventName) {
102
+ const listener = (runnable, err) => {
103
+ this.events.push(SerializableEvent.create(eventName, runnable, err));
104
+ };
105
+ return this.listeners.set(eventName, listener).get(eventName);
106
+ }
95
107
 
108
+ /**
109
+ * Creates event listeners (using {@link ParallelBuffered#createListener}) for each
110
+ * reporter-relevant event emitted by a {@link Runner}. This array is drained when
111
+ * {@link ParallelBuffered#done} is called by {@link Runner#run}.
112
+ *
113
+ * Subclasses could override this behavior.
114
+ * @public
115
+ * @param {Runner} runner - Runner instance
116
+ * @returns {ParallelBuffered}
117
+ * @chainable
118
+ */
119
+ createListeners(runner) {
96
120
  EVENT_NAMES.forEach(evt => {
97
- runner.on(evt, createListener(evt));
121
+ runner.on(evt, this.createListener(evt));
98
122
  });
99
123
  ONCE_EVENT_NAMES.forEach(evt => {
100
- runner.once(evt, createListener(evt));
124
+ runner.once(evt, this.createListener(evt));
101
125
  });
102
126
 
103
127
  runner.once(EVENT_RUN_END, () => {
104
128
  debug('received EVENT_RUN_END');
105
- listeners.forEach((listener, evt) => {
129
+ this.listeners.forEach((listener, evt) => {
106
130
  runner.removeListener(evt, listener);
107
- listeners.delete(evt);
131
+ this.listeners.delete(evt);
108
132
  });
109
133
  });
134
+
135
+ return this;
110
136
  }
111
137
 
112
138
  /**
113
139
  * Calls the {@link Mocha#run} callback (`callback`) with the test failure
114
140
  * count and the array of {@link BufferedEvent} objects. Resets the array.
141
+ *
142
+ * This is called directly by `Runner#run` and should not be called by any other consumer.
143
+ *
144
+ * Subclasses could override this.
145
+ *
115
146
  * @param {number} failures - Number of failed tests
116
147
  * @param {Function} callback - The callback passed to {@link Mocha#run}.
148
+ * @public
117
149
  */
118
150
  done(failures, callback) {
119
151
  callback(SerializableWorkerResult.create(this.events, failures));
@@ -56,7 +56,7 @@ class SerializableWorkerResult {
56
56
  /**
57
57
  * Instantiates a new {@link SerializableWorkerResult}.
58
58
  * @param {...any} args - Args to constructor
59
- * @returns {SerilizableWorkerResult}
59
+ * @returns {SerializableWorkerResult}
60
60
  */
61
61
  static create(...args) {
62
62
  return new SerializableWorkerResult(...args);
@@ -131,7 +131,11 @@ class SerializableEvent {
131
131
  */
132
132
  constructor(eventName, originalValue, originalError) {
133
133
  if (!eventName) {
134
- throw new Error('expected a non-empty `eventName` string argument');
134
+ throw createInvalidArgumentTypeError(
135
+ 'Empty `eventName` string argument',
136
+ 'eventName',
137
+ 'string'
138
+ );
135
139
  }
136
140
  /**
137
141
  * The event name.
@@ -140,8 +144,10 @@ class SerializableEvent {
140
144
  this.eventName = eventName;
141
145
  const originalValueType = type(originalValue);
142
146
  if (originalValueType !== 'object' && originalValueType !== 'undefined') {
143
- throw new Error(
144
- `expected object, received [${originalValueType}]: ${originalValue}`
147
+ throw createInvalidArgumentTypeError(
148
+ `Expected object but received ${originalValueType}`,
149
+ 'originalValue',
150
+ 'object'
145
151
  );
146
152
  }
147
153
  /**
@@ -190,7 +196,8 @@ class SerializableEvent {
190
196
  parent[key] = Object.create(null);
191
197
  return;
192
198
  }
193
- if (type(value) === 'error' || value instanceof Error) {
199
+ let _type = type(value);
200
+ if (_type === 'error') {
194
201
  // we need to reference the stack prop b/c it's lazily-loaded.
195
202
  // `__type` is necessary for deserialization to create an `Error` later.
196
203
  // `message` is apparently not enumerable, so we must handle it specifically.
@@ -200,10 +207,11 @@ class SerializableEvent {
200
207
  __type: 'Error'
201
208
  });
202
209
  parent[key] = value;
203
- // after this, the result of type(value) will be `object`, and we'll throw
210
+ // after this, set the result of type(value) to be `object`, and we'll throw
204
211
  // whatever other junk is in the original error into the new `value`.
212
+ _type = 'object';
205
213
  }
206
- switch (type(value)) {
214
+ switch (_type) {
207
215
  case 'object':
208
216
  if (type(value.serialize) === 'function') {
209
217
  parent[key] = value.serialize();
@@ -12,19 +12,13 @@ const {
12
12
  } = require('../errors');
13
13
  const workerpool = require('workerpool');
14
14
  const Mocha = require('../mocha');
15
- const {
16
- handleRequires,
17
- validatePlugin,
18
- loadRootHooks
19
- } = require('../cli/run-helpers');
15
+ const {handleRequires, validateLegacyPlugin} = require('../cli/run-helpers');
20
16
  const d = require('debug');
21
17
  const debug = d.debug(`mocha:parallel:worker:${process.pid}`);
22
18
  const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`);
23
19
  const {serialize} = require('./serializer');
24
20
  const {setInterval, clearInterval} = global;
25
21
 
26
- const BUFFERED_REPORTER_PATH = require.resolve('./reporters/parallel-buffered');
27
-
28
22
  let rootHooks;
29
23
 
30
24
  if (workerpool.isMainThread) {
@@ -45,9 +39,13 @@ if (workerpool.isMainThread) {
45
39
  * @param {Options} argv - Command-line options
46
40
  */
47
41
  let bootstrap = async argv => {
48
- const rawRootHooks = await handleRequires(argv.require);
49
- rootHooks = await loadRootHooks(rawRootHooks);
50
- validatePlugin(argv, 'ui', Mocha.interfaces);
42
+ // globalSetup and globalTeardown do not run in workers
43
+ const plugins = await handleRequires(argv.require, {
44
+ ignoredPlugins: ['mochaGlobalSetup', 'mochaGlobalTeardown']
45
+ });
46
+ validateLegacyPlugin(argv, 'ui', Mocha.interfaces);
47
+
48
+ rootHooks = plugins.rootHooks;
51
49
  bootstrap = () => {};
52
50
  debug('bootstrap(): finished with args: %O', argv);
53
51
  };
@@ -91,8 +89,6 @@ async function run(filepath, serializedOptions = '{}') {
91
89
  }
92
90
 
93
91
  const opts = Object.assign({ui: 'bdd'}, argv, {
94
- // workers only use the `Buffered` reporter.
95
- reporter: BUFFERED_REPORTER_PATH,
96
92
  // if this was true, it would cause infinite recursion.
97
93
  parallel: false,
98
94
  // this doesn't work in parallel mode
@@ -125,6 +121,7 @@ async function run(filepath, serializedOptions = '{}') {
125
121
  mocha.run(result => {
126
122
  // Runner adds these; if we don't remove them, we'll get a leak.
127
123
  process.removeAllListeners('uncaughtException');
124
+ process.removeAllListeners('unhandledRejection');
128
125
 
129
126
  try {
130
127
  const serialized = serialize(result);