Skip to content

Commit 7cd4e70

Browse files
MoLownodejs-github-bot
authored andcommitted
test_runner: support passing globs
PR-URL: #47653 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 1948dce commit 7cd4e70

File tree

6 files changed

+53
-171
lines changed

6 files changed

+53
-171
lines changed

doc/api/test.md

+16-38
Original file line numberDiff line numberDiff line change
@@ -327,52 +327,29 @@ The Node.js test runner can be invoked from the command line by passing the
327327
node --test
328328
```
329329

330-
By default, Node.js will recursively search the current directory for
331-
JavaScript source files matching a specific naming convention. Matching files
332-
are executed as test files. More information on the expected test file naming
333-
convention and behavior can be found in the [test runner execution model][]
334-
section.
330+
By default Node.js will run all files matching these patterns:
335331

336-
Alternatively, one or more paths can be provided as the final argument(s) to
337-
the Node.js command, as shown below.
332+
* `**/*.test.?(c|m)js`
333+
* `**/*-test.?(c|m)js`
334+
* `**/*_test.?(c|m)js`
335+
* `**/test-*.?(c|m)js`
336+
* `**/test.?(c|m)js`
337+
* `**/test/**/*.?(c|m)js`
338+
339+
Alternatively, one or more glob patterns can be provided as the
340+
final argument(s) to the Node.js command, as shown below.
341+
Glob patterns follow the behavior of [`glob(7)`][].
338342

339343
```bash
340-
node --test test1.js test2.mjs custom_test_dir/
344+
node --test **/*.test.js **/*.spec.js
341345
```
342346

343-
In this example, the test runner will execute the files `test1.js` and
344-
`test2.mjs`. The test runner will also recursively search the
345-
`custom_test_dir/` directory for test files to execute.
347+
Matching files are executed as test files.
348+
More information on the test file execution can be found
349+
in the [test runner execution model][] section.
346350

347351
### Test runner execution model
348352

349-
When searching for test files to execute, the test runner behaves as follows:
350-
351-
* Any files explicitly provided by the user are executed.
352-
* If the user did not explicitly specify any paths, the current working
353-
directory is recursively searched for files as specified in the following
354-
steps.
355-
* `node_modules` directories are skipped unless explicitly provided by the
356-
user.
357-
* If a directory named `test` is encountered, the test runner will search it
358-
recursively for all all `.js`, `.cjs`, and `.mjs` files. All of these files
359-
are treated as test files, and do not need to match the specific naming
360-
convention detailed below. This is to accommodate projects that place all of
361-
their tests in a single `test` directory.
362-
* In all other directories, `.js`, `.cjs`, and `.mjs` files matching the
363-
following patterns are treated as test files:
364-
* `^test$` - Files whose basename is the string `'test'`. Examples:
365-
`test.js`, `test.cjs`, `test.mjs`.
366-
* `^test-.+` - Files whose basename starts with the string `'test-'`
367-
followed by one or more characters. Examples: `test-example.js`,
368-
`test-another-example.mjs`.
369-
* `.+[\.\-\_]test$` - Files whose basename ends with `.test`, `-test`, or
370-
`_test`, preceded by one or more characters. Examples: `example.test.js`,
371-
`example-test.cjs`, `example_test.mjs`.
372-
* Other file types understood by Node.js such as `.node` and `.json` are not
373-
automatically executed by the test runner, but are supported if explicitly
374-
provided on the command line.
375-
376353
Each matching test file is executed in a separate child process. If the child
377354
process finishes with an exit code of 0, the test is considered passing.
378355
Otherwise, the test is considered to be a failure. Test files must be
@@ -2459,6 +2436,7 @@ added:
24592436
[`context.skip`]: #contextskipmessage
24602437
[`context.todo`]: #contexttodomessage
24612438
[`describe()`]: #describename-options-fn
2439+
[`glob(7)`]: https://man7.org/linux/man-pages/man7/glob.7.html
24622440
[`run()`]: #runoptions
24632441
[`test()`]: #testname-options-fn
24642442
[describe options]: #describename-options-fn

lib/internal/test_runner/runner.js

+16-60
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use strict';
22
const {
3-
ArrayFrom,
43
ArrayIsArray,
4+
ArrayPrototypeEvery,
55
ArrayPrototypeFilter,
66
ArrayPrototypeForEach,
77
ArrayPrototypeIncludes,
88
ArrayPrototypeMap,
9+
ArrayPrototypeJoin,
910
ArrayPrototypePush,
1011
ArrayPrototypeShift,
1112
ArrayPrototypeSlice,
@@ -27,7 +28,6 @@ const {
2728
} = primordials;
2829

2930
const { spawn } = require('child_process');
30-
const { readdirSync, statSync } = require('fs');
3131
const { finished } = require('internal/streams/end-of-stream');
3232
const { DefaultDeserializer, DefaultSerializer } = require('v8');
3333
// TODO(aduh95): switch to internal/readline/interface when backporting to Node.js 16.x is no longer a concern.
@@ -60,10 +60,9 @@ const {
6060
const {
6161
convertStringToRegExp,
6262
countCompletedTest,
63-
doesPathMatchFilter,
64-
isSupportedFileType,
63+
kDefaultPattern,
6564
} = require('internal/test_runner/utils');
66-
const { basename, join, resolve } = require('path');
65+
const { Glob } = require('internal/fs/glob');
6766
const { once } = require('events');
6867
const {
6968
triggerUncaughtException,
@@ -79,66 +78,23 @@ const kCanceledTests = new SafeSet()
7978

8079
let kResistStopPropagation;
8180

82-
// TODO(cjihrig): Replace this with recursive readdir once it lands.
83-
function processPath(path, testFiles, options) {
84-
const stats = statSync(path);
85-
86-
if (stats.isFile()) {
87-
if (options.userSupplied ||
88-
(options.underTestDir && isSupportedFileType(path)) ||
89-
doesPathMatchFilter(path)) {
90-
testFiles.add(path);
91-
}
92-
} else if (stats.isDirectory()) {
93-
const name = basename(path);
94-
95-
if (!options.userSupplied && name === 'node_modules') {
96-
return;
97-
}
98-
99-
// 'test' directories get special treatment. Recursively add all .js,
100-
// .cjs, and .mjs files in the 'test' directory.
101-
const isTestDir = name === 'test';
102-
const { underTestDir } = options;
103-
const entries = readdirSync(path);
104-
105-
if (isTestDir) {
106-
options.underTestDir = true;
107-
}
108-
109-
options.userSupplied = false;
110-
111-
for (let i = 0; i < entries.length; i++) {
112-
processPath(join(path, entries[i]), testFiles, options);
113-
}
114-
115-
options.underTestDir = underTestDir;
116-
}
117-
}
118-
11981
function createTestFileList() {
12082
const cwd = process.cwd();
121-
const hasUserSuppliedPaths = process.argv.length > 1;
122-
const testPaths = hasUserSuppliedPaths ?
123-
ArrayPrototypeSlice(process.argv, 1) : [cwd];
124-
const testFiles = new SafeSet();
125-
126-
try {
127-
for (let i = 0; i < testPaths.length; i++) {
128-
const absolutePath = resolve(testPaths[i]);
129-
130-
processPath(absolutePath, testFiles, { userSupplied: true });
131-
}
132-
} catch (err) {
133-
if (err?.code === 'ENOENT') {
134-
console.error(`Could not find '${err.path}'`);
135-
process.exit(kGenericUserError);
136-
}
83+
const hasUserSuppliedPattern = process.argv.length > 1;
84+
const patterns = hasUserSuppliedPattern ? ArrayPrototypeSlice(process.argv, 1) : [kDefaultPattern];
85+
const glob = new Glob(patterns, {
86+
__proto__: null,
87+
cwd,
88+
exclude: (name) => name === 'node_modules',
89+
});
90+
const results = glob.globSync();
13791

138-
throw err;
92+
if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) {
93+
console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`);
94+
process.exit(kGenericUserError);
13995
}
14096

141-
return ArrayPrototypeSort(ArrayFrom(testFiles));
97+
return ArrayPrototypeSort(results);
14298
}
14399

144100
function filterExecArgv(arg, i, arr) {

lib/internal/test_runner/utils.js

+4-11
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const {
1919
StringPrototypeSlice,
2020
} = primordials;
2121

22-
const { basename, relative } = require('path');
22+
const { relative } = require('path');
2323
const { createWriteStream } = require('fs');
2424
const { pathToFileURL } = require('internal/url');
2525
const { createDeferredPromise } = require('internal/util');
@@ -44,16 +44,10 @@ const coverageColors = {
4444

4545
const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
4646
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/;
47-
const kSupportedFileExtensions = /\.[cm]?js$/;
48-
const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/;
4947

50-
function doesPathMatchFilter(p) {
51-
return RegExpPrototypeExec(kTestFilePattern, basename(p)) !== null;
52-
}
48+
const kPatterns = ['test', 'test/**/*', 'test-*', '*[.-_]test'];
49+
const kDefaultPattern = `**/{${ArrayPrototypeJoin(kPatterns, ',')}}.?(c|m)js`;
5350

54-
function isSupportedFileType(p) {
55-
return RegExpPrototypeExec(kSupportedFileExtensions, p) !== null;
56-
}
5751

5852
function createDeferredCallback() {
5953
let calledCount = 0;
@@ -414,9 +408,8 @@ module.exports = {
414408
convertStringToRegExp,
415409
countCompletedTest,
416410
createDeferredCallback,
417-
doesPathMatchFilter,
418-
isSupportedFileType,
419411
isTestFailureError,
412+
kDefaultPattern,
420413
parseCommandLine,
421414
setupTestReporters,
422415
getCoverageReport,

test/parallel/test-runner-cli.js

+10-14
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,28 @@ const testFixtures = fixtures.path('test-runner');
2121
{
2222
// Default behavior. node_modules is ignored. Files that don't match the
2323
// pattern are ignored except in test/ directories.
24-
const args = ['--test', testFixtures];
25-
const child = spawnSync(process.execPath, args);
24+
const args = ['--test'];
25+
const child = spawnSync(process.execPath, args, { cwd: testFixtures });
2626

2727
assert.strictEqual(child.status, 1);
2828
assert.strictEqual(child.signal, null);
2929
assert.strictEqual(child.stderr.toString(), '');
3030
const stdout = child.stdout.toString();
3131
assert.match(stdout, /ok 1 - this should pass/);
3232
assert.match(stdout, /not ok 2 - this should fail/);
33-
assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/);
33+
assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/);
3434
assert.match(stdout, /ok 4 - this should pass/);
3535
}
3636

3737
{
3838
// Same but with a prototype mutation in require scripts.
39-
const args = ['--require', join(testFixtures, 'protoMutation.js'), '--test', testFixtures];
40-
const child = spawnSync(process.execPath, args);
39+
const args = ['--require', join(testFixtures, 'protoMutation.js'), '--test'];
40+
const child = spawnSync(process.execPath, args, { cwd: testFixtures });
4141

4242
const stdout = child.stdout.toString();
4343
assert.match(stdout, /ok 1 - this should pass/);
4444
assert.match(stdout, /not ok 2 - this should fail/);
45-
assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/);
45+
assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/);
4646
assert.match(stdout, /ok 4 - this should pass/);
4747
assert.strictEqual(child.status, 1);
4848
assert.strictEqual(child.signal, null);
@@ -51,23 +51,19 @@ const testFixtures = fixtures.path('test-runner');
5151

5252
{
5353
// User specified files that don't match the pattern are still run.
54-
const args = ['--test', testFixtures, join(testFixtures, 'index.js')];
55-
const child = spawnSync(process.execPath, args);
54+
const args = ['--test', join(testFixtures, 'index.js')];
55+
const child = spawnSync(process.execPath, args, { cwd: testFixtures });
5656

5757
assert.strictEqual(child.status, 1);
5858
assert.strictEqual(child.signal, null);
5959
assert.strictEqual(child.stderr.toString(), '');
6060
const stdout = child.stdout.toString();
6161
assert.match(stdout, /not ok 1 - .+index\.js/);
62-
assert.match(stdout, /ok 2 - this should pass/);
63-
assert.match(stdout, /not ok 3 - this should fail/);
64-
assert.match(stdout, /ok 4 - .+subdir.+subdir_test\.js/);
65-
assert.match(stdout, /ok 5 - this should pass/);
6662
}
6763

6864
{
6965
// Searches node_modules if specified.
70-
const args = ['--test', join(testFixtures, 'node_modules')];
66+
const args = ['--test', join(testFixtures, 'node_modules/*.js')];
7167
const child = spawnSync(process.execPath, args);
7268

7369
assert.strictEqual(child.status, 1);
@@ -89,7 +85,7 @@ const testFixtures = fixtures.path('test-runner');
8985
const stdout = child.stdout.toString();
9086
assert.match(stdout, /ok 1 - this should pass/);
9187
assert.match(stdout, /not ok 2 - this should fail/);
92-
assert.match(stdout, /ok 3 - .+subdir.+subdir_test\.js/);
88+
assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/);
9389
assert.match(stdout, /ok 4 - this should pass/);
9490
}
9591

test/parallel/test-runner-coverage.js

+7-6
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,13 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {
153153
let report = [
154154
'# start of coverage report',
155155
'# file | line % | branch % | funcs % | uncovered lines',
156-
'# test/fixtures/v8-coverage/combined_coverage/common.js | 89.86 | ' +
156+
'# common.js | 89.86 | ' +
157157
'62.50 | 100.00 | 8, 13, 14, 18, 34, 35, 53',
158-
'# test/fixtures/v8-coverage/combined_coverage/first.test.js | 83.33 | ' +
158+
'# first.test.js | 83.33 | ' +
159159
'100.00 | 50.00 | 5, 6',
160-
'# test/fixtures/v8-coverage/combined_coverage/second.test.js | 100.00 ' +
160+
'# second.test.js | 100.00 ' +
161161
'| 100.00 | 100.00 | ',
162-
'# test/fixtures/v8-coverage/combined_coverage/third.test.js | 100.00 | ' +
162+
'# third.test.js | 100.00 | ' +
163163
'100.00 | 100.00 | ',
164164
'# all files | 92.11 | 72.73 | 88.89 |',
165165
'# end of coverage report',
@@ -171,10 +171,11 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {
171171

172172
const fixture = fixtures.path('v8-coverage', 'combined_coverage');
173173
const args = [
174-
'--test', '--experimental-test-coverage', '--test-reporter', 'tap', fixture,
174+
'--test', '--experimental-test-coverage', '--test-reporter', 'tap',
175175
];
176176
const result = spawnSync(process.execPath, args, {
177-
env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }
177+
env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path },
178+
cwd: fixture,
178179
});
179180

180181
assert.strictEqual(result.stderr.toString(), '');

test/parallel/test-runner-test-filter.js

-42
This file was deleted.

0 commit comments

Comments
 (0)