Skip to content

Commit dbe229d

Browse files
khoaHyhvoxpelli
andauthored
fix: Add error handling for nonexistent file case with --file option (#5086)
* feat: handle nonexistent files passed to --file - added error handling when using the --file flag to do it the way --require does - added a test to assert that we throw the same type of error * refactor: remove path.resolve() - require.resolve() by Node.js follows a Node.js module resolution algo which includes checking if the resolved path actually exists on the file system. * add comment to new code in collect-files.js * fix: add back absolute path resolving * refactor: log warning and remove call stack * revert changes to bin/mocha.js * improve test case * throw error and have handler work with it * change collectFiles to return object * clean up * exit mocha immediately on missing file * new log message * add tests * code quality improvements * add comments * docs: update to new link name * pass mocha instance to helper function --------- Co-authored-by: Pelle Wessman <pelle@kodfabrik.se>
1 parent b9ce511 commit dbe229d

File tree

6 files changed

+190
-20
lines changed

6 files changed

+190
-20
lines changed

docs/index.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,7 @@ The option can be given multiple times. The option accepts a comma-delimited lis
10631063
10641064
`--extension` now supports multipart extensions (e.g., `spec.js`), leading dots (`.js`) and combinations thereof (`.spec.js`);
10651065

1066-
### `--file <file|directory|glob>`
1066+
### `--file <file>`
10671067

10681068
> _WARNING: `--file` is incompatible with [parallel mode](#parallel-tests)._
10691069
@@ -1298,7 +1298,7 @@ In parallel mode, Mocha does not guarantee the order in which test files will ru
12981298

12991299
Because of this, the following options, which depend on order, _cannot be used_ in parallel mode:
13001300

1301-
- [`--file`](#-file-filedirectoryglob)
1301+
- [`--file`](#-file-file)
13021302
- [`--sort`](#-sort-s)
13031303
- [`--delay`](#delayed-root-suite)
13041304
{:.single-column}

lib/cli/collect-files.js

+49-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22

3+
const fs = require('fs');
34
const path = require('path');
45
const ansi = require('ansi-colors');
56
const debug = require('debug')('mocha:cli:run:helpers');
@@ -19,7 +20,7 @@ const {castArray} = require('../utils');
1920
/**
2021
* Smash together an array of test files in the correct order
2122
* @param {FileCollectionOptions} [opts] - Options
22-
* @returns {string[]} List of files to test
23+
* @returns {FileCollectionResponse} An object containing a list of files to test and unmatched files.
2324
* @private
2425
*/
2526
module.exports = ({
@@ -30,7 +31,7 @@ module.exports = ({
3031
sort,
3132
spec
3233
} = {}) => {
33-
const unmatched = [];
34+
const unmatchedSpecFiles = [];
3435
const specFiles = spec.reduce((specFiles, arg) => {
3536
try {
3637
const moreSpecFiles = castArray(lookupFiles(arg, extension, recursive))
@@ -44,14 +45,35 @@ module.exports = ({
4445
return [...specFiles, ...moreSpecFiles];
4546
} catch (err) {
4647
if (err.code === NO_FILES_MATCH_PATTERN) {
47-
unmatched.push({message: err.message, pattern: err.pattern});
48+
unmatchedSpecFiles.push({message: err.message, pattern: err.pattern});
4849
return specFiles;
4950
}
5051

5152
throw err;
5253
}
5354
}, []);
5455

56+
// check that each file passed in to --file exists
57+
58+
const unmatchedFiles = [];
59+
fileArgs.forEach(file => {
60+
const fileAbsolutePath = path.resolve(file);
61+
try {
62+
// Used instead of fs.existsSync to ensure that file-ending less files are still resolved correctly
63+
require.resolve(fileAbsolutePath);
64+
} catch (err) {
65+
if (err.code === 'MODULE_NOT_FOUND') {
66+
unmatchedFiles.push({
67+
pattern: file,
68+
absolutePath: fileAbsolutePath
69+
});
70+
return;
71+
}
72+
73+
throw err;
74+
}
75+
});
76+
5577
// ensure we don't sort the stuff from fileArgs; order is important!
5678
if (sort) {
5779
specFiles.sort();
@@ -67,19 +89,24 @@ module.exports = ({
6789
if (!files.length) {
6890
// give full message details when only 1 file is missing
6991
const noneFoundMsg =
70-
unmatched.length === 1
71-
? `Error: No test files found: ${JSON.stringify(unmatched[0].pattern)}` // stringify to print escaped characters raw
92+
unmatchedSpecFiles.length === 1
93+
? `Error: No test files found: ${JSON.stringify(
94+
unmatchedSpecFiles[0].pattern
95+
)}` // stringify to print escaped characters raw
7296
: 'Error: No test files found';
7397
console.error(ansi.red(noneFoundMsg));
7498
process.exit(1);
7599
} else {
76100
// print messages as a warning
77-
unmatched.forEach(warning => {
101+
unmatchedSpecFiles.forEach(warning => {
78102
console.warn(ansi.yellow(`Warning: ${warning.message}`));
79103
});
80104
}
81105

82-
return files;
106+
return {
107+
files,
108+
unmatchedFiles
109+
};
83110
};
84111

85112
/**
@@ -93,3 +120,18 @@ module.exports = ({
93120
* @property {boolean} recursive - Find files recursively
94121
* @property {boolean} sort - Sort test files
95122
*/
123+
124+
/**
125+
* Diagnostic object containing unmatched files
126+
* @typedef {Object} UnmatchedFile -
127+
* @property {string} absolutePath - A list of unmatched files derived from the file arguments passed in.
128+
* @property {string} pattern - A list of unmatched files derived from the file arguments passed in.
129+
*
130+
*/
131+
132+
/**
133+
* Response object containing a list of files to test and unmatched files.
134+
* @typedef {Object} FileCollectionResponse
135+
* @property {string[]} files - A list of files to test
136+
* @property {UnmatchedFile[]} unmatchedFiles - A list of unmatched files derived from the file arguments passed in.
137+
*/

lib/cli/run-helpers.js

+47-6
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99

1010
const fs = require('fs');
1111
const path = require('path');
12+
const ansi = require('ansi-colors');
1213
const debug = require('debug')('mocha:cli:run:helpers');
1314
const {watchRun, watchParallelRun} = require('./watch-run');
1415
const collectFiles = require('./collect-files');
1516
const {format} = require('util');
1617
const {createInvalidLegacyPluginError} = require('../errors');
1718
const {requireOrImport} = require('../nodejs/esm-utils');
1819
const PluginLoader = require('../plugin-loader');
20+
const {UnmatchedFile} = require('./collect-files');
1921

2022
/**
2123
* Exits Mocha when tests + code under test has finished execution (default)
@@ -106,6 +108,32 @@ exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => {
106108
return plugins;
107109
};
108110

111+
/**
112+
* Logs errors and exits the app if unmatched files exist
113+
* @param {Mocha} mocha - Mocha instance
114+
* @param {UnmatchedFile} unmatchedFiles - object containing unmatched file paths
115+
* @returns {Promise<Runner>}
116+
* @private
117+
*/
118+
const handleUnmatchedFiles = (mocha, unmatchedFiles) => {
119+
if (unmatchedFiles.length === 0) {
120+
return;
121+
}
122+
123+
unmatchedFiles.forEach(({pattern, absolutePath}) => {
124+
console.error(
125+
ansi.yellow(
126+
`Warning: Cannot find any files matching pattern "${pattern}" at the absolute path "${absolutePath}"`
127+
)
128+
);
129+
});
130+
console.log(
131+
'No test file(s) found with the given pattern, exiting with code 1'
132+
);
133+
134+
return mocha.run(exitMocha(1));
135+
};
136+
109137
/**
110138
* Collect and load test files, then run mocha instance.
111139
* @param {Mocha} mocha - Mocha instance
@@ -117,9 +145,14 @@ exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => {
117145
* @private
118146
*/
119147
const singleRun = async (mocha, {exit}, fileCollectParams) => {
120-
const files = collectFiles(fileCollectParams);
121-
debug('single run with %d file(s)', files.length);
122-
mocha.files = files;
148+
const fileCollectionObj = collectFiles(fileCollectParams);
149+
150+
if (fileCollectionObj.unmatchedFiles.length > 0) {
151+
return handleUnmatchedFiles(mocha, fileCollectionObj.unmatchedFiles);
152+
}
153+
154+
debug('single run with %d file(s)', fileCollectionObj.files.length);
155+
mocha.files = fileCollectionObj.files;
123156

124157
// handles ESM modules
125158
await mocha.loadFilesAsync();
@@ -140,9 +173,17 @@ const singleRun = async (mocha, {exit}, fileCollectParams) => {
140173
* @private
141174
*/
142175
const parallelRun = async (mocha, options, fileCollectParams) => {
143-
const files = collectFiles(fileCollectParams);
144-
debug('executing %d test file(s) in parallel mode', files.length);
145-
mocha.files = files;
176+
const fileCollectionObj = collectFiles(fileCollectParams);
177+
178+
if (fileCollectionObj.unmatchedFiles.length > 0) {
179+
return handleUnmatchedFiles(mocha, fileCollectionObj.unmatchedFiles);
180+
}
181+
182+
debug(
183+
'executing %d test file(s) in parallel mode',
184+
fileCollectionObj.files.length
185+
);
186+
mocha.files = fileCollectionObj.files;
146187

147188
// note that we DO NOT load any files here; this is handled by the worker
148189
return mocha.run(options.exit ? exitMocha : exitMochaLater);

lib/cli/watch-run.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ exports.watchParallelRun = (
5858
newMocha.suite.ctx = new Context();
5959

6060
// reset the list of files
61-
newMocha.files = collectFiles(fileCollectParams);
61+
newMocha.files = collectFiles(fileCollectParams).files;
6262

6363
// because we've swapped out the root suite (see the `run` inner function
6464
// in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals.
@@ -120,7 +120,7 @@ exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => {
120120
newMocha.suite.ctx = new Context();
121121

122122
// reset the list of files
123-
newMocha.files = collectFiles(fileCollectParams);
123+
newMocha.files = collectFiles(fileCollectParams).files;
124124

125125
// because we've swapped out the root suite (see the `run` inner function
126126
// in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
var obj = {foo: 'bar'};
2+
3+
describe('mjs', function () {
4+
it('should work', function () {
5+
expect(obj, 'to equal', {foo: 'bar'});
6+
});
7+
});

test/integration/options/file.spec.js

+83-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
'use strict';
22

33
var path = require('path').posix;
4-
var helpers = require('../helpers');
5-
var runMochaJSON = helpers.runMochaJSON;
6-
var resolvePath = helpers.resolveFixturePath;
4+
const {
5+
runMochaJSON,
6+
resolveFixturePath: resolvePath,
7+
runMocha
8+
} = require('../helpers');
79

810
describe('--file', function () {
911
var args = [];
@@ -64,4 +66,82 @@ describe('--file', function () {
6466
done();
6567
});
6668
});
69+
70+
it('should run esm tests passed via file', function (done) {
71+
const esmFile = 'collect-files.fixture.mjs';
72+
const testArgs = ['--file', resolvePath(esmFile)];
73+
74+
runMochaJSON(esmFile, testArgs, function (err, res) {
75+
if (err) {
76+
return done(err);
77+
}
78+
expect(res, 'to have passed');
79+
done();
80+
});
81+
});
82+
83+
it('should log a warning if a nonexistent file with an unknown extension is specified', function (done) {
84+
const nonexistentTestFileArg = 'nonexistent.test.ts';
85+
runMocha(
86+
nonexistentTestFileArg,
87+
['--file'],
88+
function (err, res) {
89+
if (err) {
90+
return done(err);
91+
}
92+
93+
expect(
94+
res.output,
95+
'to contain',
96+
`Warning: Cannot find any files matching pattern`
97+
).and('to contain', nonexistentTestFileArg);
98+
done();
99+
},
100+
{stdio: 'pipe'}
101+
);
102+
});
103+
104+
it('should provide warning for nonexistent js file extensions', function (done) {
105+
const nonexistentCjsArg = 'nonexistent.test.js';
106+
107+
runMocha(
108+
nonexistentCjsArg,
109+
['--file'],
110+
function (err, res) {
111+
if (err) {
112+
return done(err);
113+
}
114+
115+
expect(
116+
res.output,
117+
'to contain',
118+
`Warning: Cannot find any files matching pattern`
119+
).and('to contain', nonexistentCjsArg);
120+
done();
121+
},
122+
{stdio: 'pipe'}
123+
);
124+
});
125+
126+
it('should provide warning for nonexistent esm file extensions', function (done) {
127+
const nonexistentEsmArg = 'nonexistent.test.mjs';
128+
129+
runMocha(
130+
nonexistentEsmArg,
131+
['--file'],
132+
function (err, res) {
133+
if (err) {
134+
return done(err);
135+
}
136+
137+
expect(
138+
res.output,
139+
'to contain',
140+
`Warning: Cannot find any files matching pattern`
141+
).and('to contain', nonexistentEsmArg);
142+
done();
143+
},
144+
{stdio: 'pipe'}
145+
);
146+
});
67147
});

0 commit comments

Comments
 (0)