Skip to content

Commit 9ab6592

Browse files
committed
Find flag for faulty numeric args before yargs parser
Signed-off-by: Dinika Saxena <[email protected]>
1 parent a8ea4dd commit 9ab6592

File tree

5 files changed

+129
-52
lines changed

5 files changed

+129
-52
lines changed

Diff for: lib/cli/options.js

+22-29
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,31 @@ const nargOpts = types.array
114114
const parse = (args = [], defaultValues = {}, ...configObjects) => {
115115
// save node-specific args for special handling.
116116
// 1. when these args have a "=" they should be considered to have values
117-
// 2. if they don't, they just boolean flags
117+
// 2. if they don't, they are just boolean flags
118118
// 3. to avoid explicitly defining the set of them, we tell yargs-parser they
119119
// are ALL boolean flags.
120120
// 4. we can then reapply the values after yargs-parser is done.
121121
const allArgs = Array.isArray(args) ? args : args.split(' ');
122-
const nodeArgs = allArgs.reduce((acc, arg) => {
123-
if (typeof arg !== 'string') {
124-
throw new Error(`Invalid option ${arg} passed to mocha cli`);
122+
const nodeArgs = allArgs.reduce((acc, arg, index, allArgs) => {
123+
const maybeFlag = allArgs[index - 1];
124+
125+
if (isNumeric(arg) && (!maybeFlag || !isMochaFlag(maybeFlag))) {
126+
throw createUnsupportedError(
127+
`Option ${arg} is unsupported by the mocha cli`
128+
);
129+
}
130+
if (
131+
isNumeric(arg) &&
132+
isMochaFlag(maybeFlag) &&
133+
expectedTypeForFlag(maybeFlag) !== 'number'
134+
) {
135+
throw createInvalidArgumentTypeError(
136+
`Mocha flag '--${maybeFlag}' given invalid option: '${arg}'`,
137+
Number(arg),
138+
expectedTypeForFlag(maybeFlag)
139+
);
125140
}
141+
126142
const pair = arg.split('=');
127143
let flag = pair[0];
128144
if (isNodeFlag(flag, false)) {
@@ -156,30 +172,6 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => {
156172
result.argv[key] = value;
157173
});
158174

159-
const numericPositionalArgs = result.argv._.filter(arg => isNumeric(arg));
160-
numericPositionalArgs.forEach(numericArg => {
161-
const flag = allArgs
162-
.map(arg => arg.replace(/^--?/, ''))
163-
.find((arg, index) => {
164-
return (
165-
isMochaFlag(arg) &&
166-
args[index + 1] === String(numericArg) &&
167-
String(result.argv[arg]) !== String(numericArg)
168-
);
169-
});
170-
if (flag) {
171-
throw createInvalidArgumentTypeError(
172-
`Flag ${flag} has invalid arg ${numericArg}`,
173-
numericArg,
174-
expectedTypeForFlag(flag)
175-
);
176-
} else {
177-
throw createUnsupportedError(
178-
`Invalid option ${numericArg} passed to mocha cli`
179-
);
180-
}
181-
});
182-
183175
return result.argv;
184176
};
185177

@@ -227,7 +219,8 @@ const loadPkgRc = (args = {}) => {
227219
filepath
228220
);
229221
} else {
230-
debug('failed to read default package.json at %s; ignoring', filepath);
222+
debug('failed to read default package.json at %s; ignoring',
223+
filepath);
231224
return result;
232225
}
233226
}

Diff for: lib/cli/run-option-metadata.js

+2-11
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,8 @@ const TYPES = (exports.types = {
5050
'sort',
5151
'watch'
5252
],
53-
number: ['retries', 'jobs'],
54-
string: [
55-
'config',
56-
'fgrep',
57-
'grep',
58-
'package',
59-
'reporter',
60-
'ui',
61-
'slow',
62-
'timeout'
63-
]
53+
number: ['retries', 'jobs', 'slow', 'timeout'],
54+
string: ['config', 'fgrep', 'grep', 'package', 'reporter', 'ui']
6455
});
6556

6657
/**

Diff for: test/node-unit/cli/mocha-flags.spec.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
const {
4+
types,
5+
expectedTypeForFlag
6+
} = require('../../../lib/cli/run-option-metadata');
7+
8+
describe('mocha-flags', function () {
9+
describe('expectedTypeForFlag()', function () {
10+
it('returns expected type for all mocha flags', function () {
11+
Object.entries(types).forEach(([dataType, flags]) => {
12+
flags.forEach(flag => {
13+
expect(expectedTypeForFlag(flag), 'to equal', dataType);
14+
});
15+
});
16+
});
17+
18+
it('returns undefined for node flags', function () {
19+
expect(expectedTypeForFlag('--throw-deprecation'), 'to equal', undefined);
20+
expect(expectedTypeForFlag('throw-deprecation'), 'to equal', undefined);
21+
});
22+
23+
it('returns undefined for unsupported flags', function () {
24+
expect(expectedTypeForFlag('--foo'), 'to equal', undefined);
25+
});
26+
});
27+
});

Diff for: test/node-unit/cli/options.spec.js

+67-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const sinon = require('sinon');
44
const rewiremock = require('rewiremock/node');
55
const {ONE_AND_DONE_ARGS} = require('../../../lib/cli/one-and-dones');
6+
const {constants} = require('../../../lib/errors');
67

78
const modulePath = require.resolve('../../../lib/cli/options');
89
const mocharcPath = require.resolve('../../../lib/mocharc.json');
@@ -204,7 +205,9 @@ describe('options', function () {
204205
const filepath = '/some/package.json';
205206
readFileSync = sinon.stub();
206207
// package.json
207-
readFileSync.onFirstCall().returns('{definitely-invalid');
208+
readFileSync
209+
.onFirstCall()
210+
.returns('{definitely-invalid');
208211
findConfig = sinon.stub().returns('/some/.mocharc.json');
209212
loadConfig = sinon.stub().returns({});
210213
findupSync = sinon.stub().returns(filepath);
@@ -222,7 +225,7 @@ describe('options', function () {
222225
loadOptions();
223226
},
224227
'to throw',
225-
/SyntaxError/
228+
/SyntaxError/,
226229
);
227230
});
228231
});
@@ -292,7 +295,7 @@ describe('options', function () {
292295

293296
result = loadOptions('--no-diff --no-config');
294297
});
295-
298+
isMocha
296299
it('should return parsed args, default config and package.json', function () {
297300
expect(
298301
result,
@@ -524,7 +527,7 @@ describe('options', function () {
524527
loadOptions('--timeout 500'),
525528
'to have property',
526529
'timeout',
527-
'500'
530+
500
528531
);
529532
});
530533

@@ -678,6 +681,23 @@ describe('options', function () {
678681
describe('"numeric arguments"', function () {
679682
const numericArg = 123;
680683

684+
const unsupportedError = arg => {
685+
return {
686+
message: `Option ${arg} is unsupported by the mocha cli`,
687+
code: constants.UNSUPPORTED
688+
};
689+
};
690+
691+
const invalidArgError = (flag, arg, expectedType = 'string') => {
692+
return {
693+
message: `Mocha flag '--${flag}' given invalid option: '${arg}'`,
694+
code: constants.INVALID_ARG_TYPE,
695+
argument: arg,
696+
actual: 'number',
697+
expected: expectedType
698+
};
699+
};
700+
681701
beforeEach(function () {
682702
readFileSync = sinon.stub();
683703
findConfig = sinon.stub();
@@ -691,16 +711,51 @@ describe('options', function () {
691711
});
692712
});
693713

694-
it('throws error when numeric option is passed to cli', function () {
695-
expect(() => loadOptions(`${numericArg}`), 'to throw', {
696-
message: `Invalid option ${numericArg} passed to mocha cli`
697-
});
714+
it('throws UNSUPPORTED error when numeric option is passed to cli', function () {
715+
expect(
716+
() => loadOptions(`${numericArg}`),
717+
'to throw',
718+
unsupportedError(numericArg)
719+
);
698720
});
699721

700-
it('throws error when numeric argument is passed to mocha flag that does not accept numeric value', function () {
701-
expect(() => loadOptions(`--delay ${numericArg}`), 'to throw', {
702-
message: `Invalid option ${numericArg} passed to mocha cli`
703-
});
722+
it('throws INVALID_ARG_TYPE error when numeric argument is passed to mocha flag that does not accept numeric value', function () {
723+
const flag = '--delay';
724+
expect(
725+
() => loadOptions(`${flag} ${numericArg}`),
726+
'to throw',
727+
invalidArgError(flag, numericArg, 'boolean')
728+
);
729+
});
730+
731+
it('throws INVALID_ARG_TYPE error when incompatible flag does not have preceding "--"', function () {
732+
const flag = 'delay';
733+
expect(
734+
() => loadOptions(`${flag} ${numericArg}`),
735+
'to throw',
736+
invalidArgError(flag, numericArg, 'boolean')
737+
);
738+
});
739+
740+
it('shows correct flag in error when multiple mocha flags have numeric values', function () {
741+
const flag = '--delay';
742+
expect(
743+
() =>
744+
loadOptions(
745+
`--timeout ${numericArg} ${flag} ${numericArg} --retries ${numericArg}`
746+
),
747+
'to throw',
748+
invalidArgError(flag, numericArg, 'boolean')
749+
);
750+
});
751+
752+
it('throws UNSUPPORTED error when numeric arg is passed to unsupported flag', function () {
753+
const invalidFlag = 'foo';
754+
expect(
755+
() => loadOptions(`${invalidFlag} ${numericArg}`),
756+
'to throw',
757+
unsupportedError(numericArg)
758+
);
704759
});
705760

706761
it('does not throw error if numeric value is passed to a compatible mocha flag', function () {

Diff for: test/node-unit/utils.spec.js

+11
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,16 @@ describe('utils', function () {
4545
);
4646
});
4747
});
48+
describe('isNumeric()', function () {
49+
it('returns true for a number type', function () {
50+
expect(utils.isNumeric(42), 'to equal', true);
51+
});
52+
it('returns true for a string that can be parsed as a number', function () {
53+
expect(utils.isNumeric('42'), 'to equal', true);
54+
});
55+
it('returns false for a string that cannot be parsed as a number', function () {
56+
expect(utils.isNumeric('foo'), 'to equal', false);
57+
});
58+
});
4859
});
4960
});

0 commit comments

Comments
 (0)