Skip to content

Commit 73c1a3e

Browse files
boneskullcraigtaub
authored andcommitted
refactor validatePlugins to throw coded errors
- add `createInvalidPluginError` for reporters, UIs, and future plugins - ensures the original error is output if the module exists, but it throws (see `test/node-unit/cli/fixtures/bad-module.fixture.js`) - remove unneeded `process.cwd()` from call to `path.resolve()` Ref: #4198
1 parent 74065ce commit 73c1a3e

File tree

4 files changed

+138
-50
lines changed

4 files changed

+138
-50
lines changed

lib/cli/run-helpers.js

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ const path = require('path');
1212
const debug = require('debug')('mocha:cli:run:helpers');
1313
const watchRun = require('./watch-run');
1414
const collectFiles = require('./collect-files');
15+
const {format} = require('util');
1516

1617
const cwd = (exports.cwd = process.cwd());
18+
const {createInvalidPluginError} = require('../errors');
1719

1820
/**
1921
* Exits Mocha when tests + code under test has finished execution (default)
@@ -146,35 +148,52 @@ exports.runMocha = async (mocha, options) => {
146148
};
147149

148150
/**
149-
* Used for `--reporter` and `--ui`. Ensures there's only one, and asserts
150-
* that it actually exists.
151-
* @todo XXX This must get run after requires are processed, as it'll prevent
152-
* interfaces from loading.
151+
* Used for `--reporter` and `--ui`. Ensures there's only one, and asserts that
152+
* it actually exists. This must be run _after_ requires are processed (see
153+
* {@link handleRequires}), as it'll prevent interfaces from loading otherwise.
153154
* @param {Object} opts - Options object
154-
* @param {string} key - Resolvable module name or path
155-
* @param {Object} [map] - An object perhaps having key `key`
155+
* @param {"reporter"|"interface"} pluginType - Type of plugin.
156+
* @param {Object} [map] - An object perhaps having key `key`. Used as a cache
157+
* of sorts; `Mocha.reporters` is one, where each key corresponds to a reporter
158+
* name
156159
* @private
157160
*/
158-
exports.validatePlugin = (opts, key, map = {}) => {
159-
if (Array.isArray(opts[key])) {
160-
throw new TypeError(`"--${key} <${key}>" can only be specified once`);
161+
exports.validatePlugin = (opts, pluginType, map = {}) => {
162+
/**
163+
* This should be a unique identifier; either a string (present in `map`),
164+
* or a resolvable (via `require.resolve`) module ID/path.
165+
* @type {string}
166+
*/
167+
const pluginId = opts[pluginType];
168+
169+
if (Array.isArray(pluginId)) {
170+
throw createInvalidPluginError(
171+
`"--${pluginType}" can only be specified once`,
172+
pluginType
173+
);
161174
}
162175

163-
const unknownError = () => new Error(`Unknown "${key}": ${opts[key]}`);
176+
const unknownError = err =>
177+
createInvalidPluginError(
178+
format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err),
179+
pluginType,
180+
pluginId
181+
);
164182

165-
if (!map[opts[key]]) {
183+
// if this exists, then it's already loaded, so nothing more to do.
184+
if (!map[pluginId]) {
166185
try {
167-
opts[key] = require(opts[key]);
186+
opts[pluginType] = require(pluginId);
168187
} catch (err) {
169188
if (err.code === 'MODULE_NOT_FOUND') {
170189
// Try to load reporters from a path (absolute or relative)
171190
try {
172-
opts[key] = require(path.resolve(process.cwd(), opts[key]));
191+
opts[pluginType] = require(path.resolve(pluginId));
173192
} catch (err) {
174-
throw unknownError();
193+
throw unknownError(err);
175194
}
176195
} else {
177-
throw unknownError();
196+
throw unknownError(err);
178197
}
179198
}
180199
}

lib/errors.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,26 @@ function createInvalidExceptionError(message, value) {
129129
return err;
130130
}
131131

132+
/**
133+
* Dynamically creates a plugin-type-specific error based on plugin type
134+
* @param {string} message - Error message
135+
* @param {"reporter"|"interface"} pluginType - Plugin type. Future: expand as needed
136+
* @param {string} [pluginId] - Name/path of plugin, if any
137+
* @throws When `pluginType` is not known
138+
* @public
139+
* @returns {Error}
140+
*/
141+
function createInvalidPluginError(message, pluginType, pluginId) {
142+
switch (pluginType) {
143+
case 'reporter':
144+
return createInvalidReporterError(message, pluginId);
145+
case 'interface':
146+
return createInvalidInterfaceError(message, pluginId);
147+
default:
148+
throw new Error('unknown pluginType "' + pluginType + '"');
149+
}
150+
}
151+
132152
module.exports = {
133153
createInvalidArgumentTypeError: createInvalidArgumentTypeError,
134154
createInvalidArgumentValueError: createInvalidArgumentValueError,
@@ -137,5 +157,6 @@ module.exports = {
137157
createInvalidReporterError: createInvalidReporterError,
138158
createMissingArgumentError: createMissingArgumentError,
139159
createNoFilesMatchPatternError: createNoFilesMatchPatternError,
140-
createUnsupportedError: createUnsupportedError
160+
createUnsupportedError: createUnsupportedError,
161+
createInvalidPluginError: createInvalidPluginError
141162
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error('this module is wonky');

test/node-unit/cli/run-helpers.spec.js

Lines changed: 81 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,96 @@
11
'use strict';
22

33
const {validatePlugin, list} = require('../../../lib/cli/run-helpers');
4-
const {createSandbox} = require('sinon');
54

6-
describe('cli "run" command', function() {
7-
let sandbox;
5+
describe('run helper functions', function() {
6+
describe('validatePlugin()', function() {
7+
describe('when used with "reporter" key', function() {
8+
it('should disallow an array of names', function() {
9+
expect(
10+
() => validatePlugin({reporter: ['bar']}, 'reporter'),
11+
'to throw',
12+
{
13+
code: 'ERR_MOCHA_INVALID_REPORTER',
14+
message: /can only be specified once/i
15+
}
16+
);
17+
});
818

9-
beforeEach(function() {
10-
sandbox = createSandbox();
11-
});
19+
it('should fail to recognize an unknown reporter', function() {
20+
expect(
21+
() => validatePlugin({reporter: 'bar'}, 'reporter'),
22+
'to throw',
23+
{code: 'ERR_MOCHA_INVALID_REPORTER', message: /cannot find module/i}
24+
);
25+
});
26+
});
1227

13-
afterEach(function() {
14-
sandbox.restore();
15-
});
28+
describe('when used with an "interfaces" key', function() {
29+
it('should disallow an array of names', function() {
30+
expect(
31+
() => validatePlugin({interface: ['bar']}, 'interface'),
32+
'to throw',
33+
{
34+
code: 'ERR_MOCHA_INVALID_INTERFACE',
35+
message: /can only be specified once/i
36+
}
37+
);
38+
});
1639

17-
describe('helpers', function() {
18-
describe('validatePlugin()', function() {
19-
it('should disallow an array of module names', function() {
40+
it('should fail to recognize an unknown interface', function() {
2041
expect(
21-
() => validatePlugin({foo: ['bar']}, 'foo'),
22-
'to throw a',
23-
TypeError
42+
() => validatePlugin({interface: 'bar'}, 'interface'),
43+
'to throw',
44+
{code: 'ERR_MOCHA_INVALID_INTERFACE', message: /cannot find module/i}
2445
);
2546
});
2647
});
2748

28-
describe('list()', function() {
29-
describe('when provided a flat array', function() {
30-
it('should return a flat array', function() {
31-
expect(list(['foo', 'bar']), 'to equal', ['foo', 'bar']);
32-
});
33-
});
34-
describe('when provided a nested array', function() {
35-
it('should return a flat array', function() {
36-
expect(list([['foo', 'bar'], 'baz']), 'to equal', [
37-
'foo',
38-
'bar',
39-
'baz'
40-
]);
41-
});
42-
});
43-
describe('when given a comma-delimited string', function() {
44-
it('should return a flat array', function() {
45-
expect(list('foo,bar'), 'to equal', ['foo', 'bar']);
46-
});
49+
describe('when used with an unknown plugin type', function() {
50+
it('should fail', function() {
51+
expect(
52+
() => validatePlugin({frog: 'bar'}, 'frog'),
53+
'to throw',
54+
/unknown plugin/i
55+
);
56+
});
57+
});
58+
59+
describe('when a plugin throws an exception upon load', function() {
60+
it('should fail and report the original error', function() {
61+
expect(
62+
() =>
63+
validatePlugin(
64+
{
65+
reporter: require.resolve('./fixtures/bad-module.fixture.js')
66+
},
67+
'reporter'
68+
),
69+
'to throw',
70+
{message: /wonky/, code: 'ERR_MOCHA_INVALID_REPORTER'}
71+
);
72+
});
73+
});
74+
});
75+
76+
describe('list()', function() {
77+
describe('when provided a flat array', function() {
78+
it('should return a flat array', function() {
79+
expect(list(['foo', 'bar']), 'to equal', ['foo', 'bar']);
80+
});
81+
});
82+
describe('when provided a nested array', function() {
83+
it('should return a flat array', function() {
84+
expect(list([['foo', 'bar'], 'baz']), 'to equal', [
85+
'foo',
86+
'bar',
87+
'baz'
88+
]);
89+
});
90+
});
91+
describe('when given a comma-delimited string', function() {
92+
it('should return a flat array', function() {
93+
expect(list('foo,bar'), 'to equal', ['foo', 'bar']);
4794
});
4895
});
4996
});

0 commit comments

Comments
 (0)