-
-
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.
- refactoring for `lib/worker.js` - fixed some docstrings
- Loading branch information
Showing
2 changed files
with
262 additions
and
40 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,63 +1,99 @@ | ||
'use strict'; | ||
|
||
const {createInvalidArgumentTypeError} = require('./errors'); | ||
const workerpool = require('workerpool'); | ||
const Mocha = require('./mocha'); | ||
const {handleRequires, validatePlugin} = require('./cli/run-helpers'); | ||
const debug = require('debug')('mocha:worker'); | ||
const debug = require('debug')(`mocha:worker:${process.pid}`); | ||
const {serialize} = require('./serializer'); | ||
let bootstrapped = false; | ||
|
||
const BUFFERED_REPORTER_PATH = require.resolve('./reporters/buffered'); | ||
|
||
if (workerpool.isMainThread) { | ||
throw new Error( | ||
'This script is intended to be run as a worker (by the `workerpool` package).' | ||
); | ||
} | ||
|
||
/** | ||
* Initializes some stuff on the first call to {@link run}. | ||
* | ||
* Handles `--require` and `--ui`. Does _not_ handle `--reporter`, | ||
* as only the `Buffered` reporter is used. | ||
* | ||
* **This function only runs once**; it overwrites itself with a no-op | ||
* before returning. | ||
* | ||
* @param {Options} argv - Command-line options | ||
*/ | ||
let bootstrap = argv => { | ||
handleRequires(argv.require); | ||
validatePlugin(argv, 'ui', Mocha.interfaces); | ||
process.on('beforeExit', () => { | ||
/* istanbul ignore next */ | ||
debug('exiting'); | ||
}); | ||
debug('bootstrapped'); | ||
bootstrap = () => {}; | ||
}; | ||
|
||
/** | ||
* Runs a single test file in a worker thread. | ||
* @param {string} file - Filepath of test file | ||
* @param {Options} argv - Parsed command-line options object | ||
* @returns {Promise<[number, BufferedEvent[]]>} A tuple of failures and | ||
* serializable event data | ||
* @param {string} filepath - Filepath of test file | ||
* @param {Options} [argv] - Parsed command-line options object | ||
* @returns {Promise<{failures: number, events: BufferedEvent[]}>} - Test | ||
* failure count and list of events. | ||
*/ | ||
async function run(file, argv) { | ||
debug('running test file %s on process [%d]', file, process.pid); | ||
// the buffered reporter retains its events; these events are returned | ||
// from this function back to the main process. | ||
argv.reporter = require.resolve('./reporters/buffered'); | ||
// if these were set, it would cause infinite recursion by spawning another worker | ||
delete argv.parallel; | ||
delete argv.jobs; | ||
if (!bootstrapped) { | ||
// setup requires and ui, but only do this once--we will reuse this worker! | ||
handleRequires(argv.require); | ||
validatePlugin(argv, 'ui', Mocha.interfaces); | ||
bootstrapped = true; | ||
debug('bootstrapped process [%d]', process.pid); | ||
async function run(filepath, argv = {ui: 'bdd'}) { | ||
if (!filepath) { | ||
throw createInvalidArgumentTypeError( | ||
'Expected a non-empty "filepath" argument', | ||
'file', | ||
'string' | ||
); | ||
} | ||
const mocha = new Mocha(argv); | ||
mocha.files = [file]; | ||
|
||
debug('running test file %s', filepath); | ||
|
||
const opts = Object.assign(argv, { | ||
// workers only use the `Buffered` reporter. | ||
reporter: BUFFERED_REPORTER_PATH, | ||
// if this was true, it would cause infinite recursion. | ||
parallel: false | ||
}); | ||
|
||
bootstrap(opts); | ||
|
||
const mocha = new Mocha(opts).addFile(filepath); | ||
|
||
try { | ||
await mocha.loadFilesAsync(); | ||
} catch (err) { | ||
debug( | ||
'process [%d] rejecting; could not load file %s: %s', | ||
process.pid, | ||
file, | ||
err | ||
); | ||
debug('could not load file %s: %s', filepath, err); | ||
throw err; | ||
} | ||
return new Promise(resolve => { | ||
// TODO: figure out exactly what the sad path looks like here. | ||
// will depend on allowUncaught | ||
// rejection should only happen if an error is "unrecoverable" | ||
|
||
return new Promise((resolve, reject) => { | ||
mocha.run(result => { | ||
// Runner adds these; if we don't remove them, we'll get a leak. | ||
process.removeAllListeners('uncaughtException'); | ||
debug('process [%d] resolving', process.pid); | ||
resolve(serialize(result)); | ||
|
||
debug('completed run with %d test failures', result.failures); | ||
try { | ||
resolve(serialize(result)); | ||
} catch (err) { | ||
// TODO: figure out exactly what the sad path looks like here. | ||
// rejection should only happen if an error is "unrecoverable" | ||
reject(err); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
workerpool.worker({ | ||
run | ||
}); | ||
// this registers the `run` function. | ||
workerpool.worker({run}); | ||
|
||
debug('running'); | ||
|
||
process.on('beforeExit', () => { | ||
debug('process [%d] exiting', process.pid); | ||
}); | ||
// for testing | ||
exports.run = run; |
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,186 @@ | ||
'use strict'; | ||
|
||
const {SerializableWorkerResult} = require('../../lib/serializer'); | ||
const rewiremock = require('rewiremock/node'); | ||
const {createSandbox} = require('sinon'); | ||
|
||
const WORKER_PATH = require.resolve('../../lib/worker.js'); | ||
|
||
describe('worker', function() { | ||
let worker; | ||
let workerpoolWorker; | ||
let sandbox; | ||
|
||
beforeEach(function() { | ||
sandbox = createSandbox(); | ||
workerpoolWorker = sandbox.stub(); | ||
sandbox.spy(process, 'removeAllListeners'); | ||
}); | ||
|
||
describe('when run as main "thread"', function() { | ||
it('should throw', function() { | ||
expect(() => { | ||
rewiremock.proxy(WORKER_PATH, { | ||
workerpool: { | ||
isMainThread: true, | ||
worker: workerpoolWorker | ||
} | ||
}); | ||
}, 'to throw'); | ||
}); | ||
}); | ||
|
||
describe('when run as "worker thread"', function() { | ||
class MockMocha {} | ||
let serializer; | ||
let runHelpers; | ||
|
||
beforeEach(function() { | ||
MockMocha.prototype.addFile = sandbox.stub().returnsThis(); | ||
MockMocha.prototype.loadFilesAsync = sandbox.stub(); | ||
MockMocha.prototype.run = sandbox.stub(); | ||
MockMocha.interfaces = { | ||
bdd: sandbox.stub() | ||
}; | ||
|
||
serializer = { | ||
serialize: sandbox.stub() | ||
}; | ||
|
||
runHelpers = { | ||
handleRequires: sandbox.stub(), | ||
validatePlugin: sandbox.stub() | ||
}; | ||
|
||
worker = rewiremock.proxy(WORKER_PATH, { | ||
workerpool: { | ||
isMainThread: false, | ||
worker: workerpoolWorker | ||
}, | ||
'../../lib/mocha': MockMocha, | ||
'../../lib/serializer': serializer, | ||
'../../lib/cli/run-helpers': runHelpers | ||
}); | ||
}); | ||
|
||
it('should register itself with workerpool', function() { | ||
expect(workerpoolWorker, 'to have a call satisfying', [ | ||
{run: worker.run} | ||
]); | ||
}); | ||
|
||
describe('function', function() { | ||
describe('run', function() { | ||
describe('when called without arguments', function() { | ||
it('should reject', async function() { | ||
return expect(worker.run, 'to be rejected with error satisfying', { | ||
code: 'ERR_MOCHA_INVALID_ARG_TYPE' | ||
}); | ||
}); | ||
}); | ||
|
||
describe('when called with empty "filepath" argument', function() { | ||
it('should reject', async function() { | ||
return expect( | ||
() => worker.run(''), | ||
'to be rejected with error satisfying', | ||
{ | ||
code: 'ERR_MOCHA_INVALID_ARG_TYPE' | ||
} | ||
); | ||
}); | ||
}); | ||
|
||
describe('when the file at "filepath" argument is unloadable', function() { | ||
it('should reject', async function() { | ||
MockMocha.prototype.loadFilesAsync.rejects(); | ||
return expect( | ||
() => worker.run('some-non-existent-file.js'), | ||
'to be rejected' | ||
); | ||
}); | ||
}); | ||
|
||
describe('when the file at "filepath" is loadable', function() { | ||
let result; | ||
beforeEach(function() { | ||
result = SerializableWorkerResult.create(); | ||
|
||
MockMocha.prototype.loadFilesAsync.resolves(); | ||
MockMocha.prototype.run.yields(result); | ||
}); | ||
|
||
it('should handle "--require"', async function() { | ||
await worker.run('some-file.js', {require: 'foo'}); | ||
expect(runHelpers.handleRequires, 'to have a call satisfying', [ | ||
'foo' | ||
]).and('was called once'); | ||
}); | ||
|
||
it('should handle "--ui"', async function() { | ||
const argv = {}; | ||
await worker.run('some-file.js', argv); | ||
|
||
expect(runHelpers.validatePlugin, 'to have a call satisfying', [ | ||
argv, | ||
'ui', | ||
MockMocha.interfaces | ||
]).and('was called once'); | ||
}); | ||
|
||
it('should call Mocha#run', async function() { | ||
await worker.run('some-file.js'); | ||
expect(MockMocha.prototype.run, 'was called once'); | ||
}); | ||
|
||
it('should remove all uncaughtException listeners', async function() { | ||
await worker.run('some-file.js'); | ||
expect(process.removeAllListeners, 'to have a call satisfying', [ | ||
'uncaughtException' | ||
]); | ||
}); | ||
|
||
describe('when serialization succeeds', function() { | ||
beforeEach(function() { | ||
serializer.serialize.returnsArg(0); | ||
}); | ||
|
||
it('should resolve with a SerializedWorkerResult', async function() { | ||
return expect( | ||
worker.run('some-file.js'), | ||
'to be fulfilled with', | ||
result | ||
); | ||
}); | ||
}); | ||
|
||
describe('when serialization fails', function() { | ||
beforeEach(function() { | ||
serializer.serialize.throws(); | ||
}); | ||
|
||
it('should reject', async function() { | ||
return expect(worker.run('some-file.js'), 'to be rejected'); | ||
}); | ||
}); | ||
|
||
describe('when run twice', function() { | ||
it('should initialize only once', async function() { | ||
await worker.run('some-file.js'); | ||
await worker.run('some-other-file.js'); | ||
|
||
expect(runHelpers, 'to satisfy', { | ||
handleRequires: expect.it('was called once'), | ||
validatePlugin: expect.it('was called once') | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
afterEach(function() { | ||
sandbox.restore(); | ||
}); | ||
}); |