-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: concurrency based on worker threads; see #2839 [ci skip]
- Loading branch information
Showing
9 changed files
with
290 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
'use strict'; | ||
|
||
const Runner = require('./runner'); | ||
const {EVENT_RUN_BEGIN, EVENT_RUN_END} = Runner.constants; | ||
const {spawn, Pool, Worker} = require('threads'); | ||
const debug = require('debug')('mocha:buffered-runner'); | ||
|
||
/** | ||
* This `Runner` delegates tests runs to worker threads. Does not execute any | ||
* {@link Runnable}s by itself! | ||
*/ | ||
class BufferedRunner extends Runner { | ||
/** | ||
* Runs Mocha tests by creating a thread pool, then delegating work to the | ||
* worker threads. Each worker receives one file, and as workers become | ||
* available, they take a file from the queue and run it. | ||
* The worker thread execution is treated like an RPC--it returns a `Promise` | ||
* containing serialized information about the run. The information is processed | ||
* as it's received, and emitted to a {@link Reporter}, which is likely listening | ||
* for these events. | ||
* | ||
* @todo handle tests in a specific order, e.g., via `--file`? | ||
* @todo handle delayed runs? | ||
* @todo graceful failure | ||
* @todo audit `BufferedEvent` objects; e.g. do tests need a `parent` prop? | ||
* @todo should we just instantiate a `Test` object from the `BufferedEvent`? | ||
* @param {Function} callback - Called with an exit code corresponding to | ||
* number of test failures. | ||
* @param {Object} options | ||
* @param {string[]} options.files - List of test files | ||
* @param {Options} option.opts - Command-line options | ||
* @returns {Promise<void>} | ||
*/ | ||
async run(callback, {files, opts}) { | ||
const pool = Pool(() => spawn(new Worker('./worker.js')), opts.jobs); | ||
|
||
let exitCode = 0; | ||
|
||
this.emit(EVENT_RUN_BEGIN); | ||
|
||
files.forEach(file => { | ||
debug('enqueueing test file %s', file); | ||
pool.queue(async run => { | ||
const [failures, events] = await run(file, opts); | ||
debug( | ||
'completed run of file %s; %d failures / %d events', | ||
file, | ||
failures, | ||
events.length | ||
); | ||
exitCode += failures; // can this be non-numeric? | ||
events.forEach(({name, data}) => { | ||
Object.keys(data).forEach(key => { | ||
if (key.startsWith('__')) { | ||
data[key.slice(2)] = () => data[key]; | ||
} | ||
}); | ||
// maybe we should just expect `err` separately from the worker. | ||
if (data.err) { | ||
this.emit(name, data, data.err); | ||
} else { | ||
this.emit(name, data); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
await pool.settled(); // nonzero exit code if rejection? | ||
await pool.terminate(); | ||
this.emit(EVENT_RUN_END); | ||
callback(exitCode); | ||
} | ||
} | ||
|
||
module.exports = BufferedRunner; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
'use strict'; | ||
/** | ||
* @module Buffered | ||
*/ | ||
/** | ||
* Module dependencies. | ||
*/ | ||
|
||
const { | ||
EVENT_SUITE_BEGIN, | ||
EVENT_SUITE_END, | ||
EVENT_TEST_FAIL, | ||
EVENT_TEST_PASS, | ||
EVENT_TEST_PENDING | ||
} = require('../runner').constants; | ||
|
||
/** | ||
* Creates a {@link BufferedEvent} from a {@link Suite}. | ||
* @param {string} evt - Event name | ||
* @param {Suite} suite - Suite object | ||
* @returns {BufferedEvent} | ||
*/ | ||
const serializeSuite = (evt, suite) => ({ | ||
name: evt, | ||
data: {root: suite.root, title: suite.title} | ||
}); | ||
|
||
/** | ||
* Creates a {@link BufferedEvent} from a {@link Test}. | ||
* @param {string} evt - Event name | ||
* @param {Test} test - Test object | ||
* @param {any} err - Error, if applicable | ||
*/ | ||
const serializeTest = (evt, test, [err]) => { | ||
const obj = { | ||
title: test.title, | ||
duration: test.duration, | ||
err: test.err, | ||
__fullTitle: test.fullTitle(), | ||
__slow: test.slow(), | ||
__titlePath: test.titlePath() | ||
}; | ||
if (err) { | ||
obj.err = | ||
test.err && err instanceof Error | ||
? { | ||
multiple: [...(test.err.multiple || []), err] | ||
} | ||
: err; | ||
} | ||
return { | ||
name: evt, | ||
data: obj | ||
}; | ||
}; | ||
|
||
/** | ||
* The `Buffered` reporter is for use by parallel runs. Instead of outputting | ||
* to `STDOUT`, etc., it retains a list of events it receives and hands these | ||
* off to the callback passed into {@link Mocha#run}. That callback will then | ||
* return the data to the main process. | ||
*/ | ||
class Buffered { | ||
/** | ||
* Listens for {@link Runner} events and retains them in an `events` instance prop. | ||
* @param {Runner} runner | ||
*/ | ||
constructor(runner) { | ||
/** | ||
* Retained list of events emitted from the {@link Runner} instance. | ||
* @type {BufferedEvent[]} | ||
*/ | ||
const events = (this.events = []); | ||
|
||
runner | ||
.on(EVENT_SUITE_BEGIN, suite => { | ||
events.push(serializeSuite(EVENT_SUITE_BEGIN, suite)); | ||
}) | ||
.on(EVENT_SUITE_END, suite => { | ||
events.push(serializeSuite(EVENT_SUITE_END, suite)); | ||
}) | ||
.on(EVENT_TEST_PENDING, test => { | ||
events.push(serializeTest(EVENT_TEST_PENDING, test)); | ||
}) | ||
.on(EVENT_TEST_FAIL, (test, err) => { | ||
events.push(serializeTest(EVENT_TEST_FAIL, test, err)); | ||
}) | ||
.on(EVENT_TEST_PASS, test => { | ||
events.push(serializeTest(EVENT_TEST_PASS, test)); | ||
}); | ||
} | ||
|
||
/** | ||
* Calls the {@link Mocha#run} callback (`callback`) with the test failure | ||
* count and the array of {@link BufferedEvent} objects. Resets the array. | ||
* @param {number} failures - Number of failed tests | ||
* @param {Function} callback - The callback passed to {@link Mocha#run}. | ||
*/ | ||
done(failures, callback) { | ||
callback(failures, [...this.events]); | ||
this.events = []; | ||
} | ||
} | ||
|
||
/** | ||
* Serializable event data from a `Runner`. Keys of the `data` property | ||
* beginning with `__` will be converted into a function which returns the value | ||
* upon deserialization. | ||
* @typedef {Object} BufferedEvent | ||
* @property {string} name - Event name | ||
* @property {object} data - Event parameters | ||
*/ | ||
|
||
module.exports = Buffered; |
Oops, something went wrong.