diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index dc6529c220539a..e1ffdc921d0090 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -34,9 +34,10 @@ const { } = primordials; const { spawn } = require('child_process'); +const { statSync } = require('fs'); const { finished } = require('internal/streams/end-of-stream'); const { availableParallelism } = require('os'); -const { resolve, sep, isAbsolute } = require('path'); +const { resolve, sep, isAbsolute, join } = require('path'); const { DefaultDeserializer, DefaultSerializer } = require('v8'); const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options'); const { Interface } = require('internal/readline/interface'); @@ -154,11 +155,41 @@ function createTestFileList(patterns, cwd) { if (!patterns || patterns.length === 0) { patterns = [kDefaultPattern]; } - const glob = new Glob(patterns, { + const globOptions = { __proto__: null, cwd, exclude: (name) => name === 'node_modules', + }; + let glob = new Glob(patterns, globOptions); + + // Expand plain directory arguments into a recursive search for test files + // within them. A pattern with no glob magic that resolves to an existing + // directory (e.g. `node --test test/`) would otherwise match the directory + // itself and then be spawned as a test file, failing with MODULE_NOT_FOUND. + // This restores the directory-argument behavior that was documented and + // supported before test discovery moved to globbing. + // Refs: https://github.com/nodejs/node/issues/64555 + let didExpandDirectory = false; + const expandedPatterns = ArrayPrototypeMap(patterns, (pattern, index) => { + if (glob.matchers[index].hasMagic()) { + return pattern; + } + let stats; + try { + stats = statSync(resolve(cwd, pattern)); + } catch { + return pattern; + } + if (stats.isDirectory()) { + didExpandDirectory = true; + return join(pattern, kDefaultPattern); + } + return pattern; }); + if (didExpandDirectory) { + glob = new Glob(expandedPatterns, globOptions); + } + const results = glob.globSync(); if (hasUserSuppliedPattern && results.length === 0 && ArrayPrototypeEvery(glob.matchers, (m) => !m.hasMagic())) { diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index f563d67d1a9a41..d1d61f0799cd1c 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -161,6 +161,34 @@ for (const isolation of ['none', 'process']) { assert.strictEqual(child.signal, null); } + { + // A directory argument recursively runs the test files inside it, the same + // as running from that directory. Both the bare and trailing-slash forms + // are supported. Refs: https://github.com/nodejs/node/issues/64555 + for (const dir of ['default-behavior', 'default-behavior/']) { + const args = [ + '--test', + '--test-reporter=tap', + `--test-isolation=${isolation}`, + dir, + ]; + const child = spawnSync(process.execPath, args, { cwd: testFixtures }); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /ok 1 - this should pass/); + assert.match(stdout, /not ok 2 - this should fail/); + assert.match(stdout, /ok 3 - subdir.+subdir_test\.js/); + assert.match(stdout, /ok 4 - this should pass/); + assert.match(stdout, /ok 5 - this should be skipped/); + assert.match(stdout, /ok 6 - this should be executed/); + // node_modules is still excluded when a directory is expanded. + assert.doesNotMatch(stdout, /test-nm\.js/); + assert.strictEqual(child.status, 1); + assert.strictEqual(child.signal, null); + } + } + { // Test combined stream outputs const args = [