Skip to content

Commit

Permalink
add unit-ish tests for worker
Browse files Browse the repository at this point in the history
- refactoring for `lib/worker.js`
- fixed some docstrings
  • Loading branch information
boneskull committed Mar 26, 2020
1 parent 7ad6d70 commit 09cb328
Show file tree
Hide file tree
Showing 2 changed files with 262 additions and 40 deletions.
116 changes: 76 additions & 40 deletions lib/worker.js
@@ -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;
186 changes: 186 additions & 0 deletions test/node-unit/worker.spec.js
@@ -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();
});
});

0 comments on commit 09cb328

Please sign in to comment.