Skip to content

Commit b4101a3

Browse files
committed
test_runner: preserve literal directory arguments
Search existing directories with the directory as the glob cwd instead of appending the default glob to the path. This prevents brackets and braces in directory names from changing which tests are selected. Preserve glob arguments, node_modules filtering, and report paths. Deduplicate overlapping selections by their resolved paths. Assisted-by: Codex Signed-off-by: Casper Verbuyst <casperverbuyst@gmail.com>
1 parent 1c90519 commit b4101a3

2 files changed

Lines changed: 39 additions & 13 deletions

File tree

‎lib/internal/test_runner/runner.js‎

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayFrom,
45
ArrayIsArray,
56
ArrayPrototypeFilter,
67
ArrayPrototypeFind,
@@ -17,7 +18,6 @@ const {
1718
ObjectAssign,
1819
PromisePrototypeThen,
1920
PromiseWithResolvers,
20-
RegExpPrototypeSymbolReplace,
2121
SafeMap,
2222
SafePromiseAll,
2323
SafePromiseAllReturnVoid,
@@ -36,7 +36,7 @@ const {
3636
const { spawn } = require('child_process');
3737
const { statSync } = require('fs');
3838
const { finished } = require('internal/streams/end-of-stream');
39-
const { resolve, sep, isAbsolute } = require('path');
39+
const { resolve, sep, isAbsolute, relative } = require('path');
4040
const { DefaultDeserializer, DefaultSerializer } = require('v8');
4141
const { getOptionValue, getOptionsAsFlagsFromBinding } = require('internal/options');
4242
const { Interface } = require('internal/readline/interface');
@@ -155,39 +155,54 @@ class WorkerIdPool {
155155

156156
function createTestFileList(patterns, cwd) {
157157
const hasUserSuppliedPattern = patterns != null;
158+
const directories = new SafeMap();
158159
if (!patterns || patterns.length === 0) {
159160
patterns = [kDefaultPattern];
160161
} else {
161-
patterns = ArrayPrototypeMap(patterns, (pattern) => {
162-
// A directory argument is expanded to search for the default test files
163-
// within it, matching the behavior from before glob patterns were
164-
// supported. Glob patterns always use forward slashes, so a trailing
165-
// path separator (of either kind) is stripped before appending.
162+
patterns = ArrayPrototypeFilter(patterns, (pattern) => {
166163
const resolved = isAbsolute(pattern) ? pattern : resolve(cwd, pattern);
167164
try {
168165
if (statSync(resolved).isDirectory()) {
169-
return `${RegExpPrototypeSymbolReplace(/[\\/]+$/, pattern, '')}/${kDefaultPattern}`;
166+
directories.set(pattern, resolved);
167+
return false;
170168
}
171169
} catch {
172170
// Not a directory or not accessible; leave the pattern as-is so the
173171
// existing "Could not find" handling still applies.
174172
}
175-
return pattern;
173+
return true;
176174
});
177175
}
178176
const glob = new Glob(patterns, {
179177
__proto__: null,
180178
cwd,
181179
exclude: (name) => name === 'node_modules',
182180
});
183-
const results = glob.globSync();
181+
// De-duplicate by absolute path while preserving the paths used in reports.
182+
const results = new SafeMap(ArrayPrototypeMap(glob.globSync(), (file) => [resolve(cwd, file), file]));
183+
for (const { 0: pattern, 1: directory } of directories) {
184+
// Preserve the exclusion of node_modules in the directory argument itself.
185+
if (!isAbsolute(pattern) && relative(cwd, directory) === 'node_modules') continue;
186+
// Search from the directory so its name is never interpreted as a glob.
187+
const directoryGlob = new Glob(kDefaultPattern, {
188+
__proto__: null,
189+
cwd: directory,
190+
exclude: (name) => name === 'node_modules',
191+
});
192+
for (const file of directoryGlob.globSync()) {
193+
const absolutePath = resolve(directory, file);
194+
if (!results.has(absolutePath)) {
195+
results.set(absolutePath, isAbsolute(pattern) ? absolutePath : relative(cwd, absolutePath));
196+
}
197+
}
198+
}
184199

185-
if (hasUserSuppliedPattern && results.length === 0 && !glob.hasMagic) {
200+
if (hasUserSuppliedPattern && results.size === 0 && directories.size === 0 && !glob.hasMagic) {
186201
console.error(`Could not find '${ArrayPrototypeJoin(patterns, ', ')}'`);
187202
process.exit(kGenericUserError);
188203
}
189204

190-
return ArrayPrototypeSort(results);
205+
return ArrayPrototypeSort(ArrayFrom(results.values()));
191206
}
192207

193208
function filterExecArgv(arg, i, arr) {

‎test/parallel/test-runner-cli.js‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@
33
require('../common');
44
const assert = require('assert');
55
const { spawnSync } = require('child_process');
6+
const { cpSync } = require('fs');
67
const { spawnSyncAndAssert } = require('../common/child_process');
78
const { join } = require('path');
89
const fixtures = require('../common/fixtures');
10+
const tmpdir = require('../common/tmpdir');
911
const testFixtures = fixtures.path('test-runner');
1012

13+
tmpdir.refresh();
14+
const literalDirs = ['matching-patterns[1]', 'matching-patterns{1,2}'].map((dir) => join(tmpdir.path, dir));
15+
for (const dir of literalDirs) {
16+
cpSync(join(testFixtures, 'matching-patterns'), dir, { recursive: true });
17+
}
18+
// This sibling must not be selected by treating the directory as a glob.
19+
cpSync(join(testFixtures, 'default-behavior'), join(tmpdir.path, 'matching-patterns1'), { recursive: true });
20+
1121
for (const isolation of ['none', 'process']) {
1222
{
1323
// File not found.
@@ -67,7 +77,8 @@ for (const isolation of ['none', 'process']) {
6777
// A directory argument is searched for the default test files within it,
6878
// both bare and with a trailing separator.
6979
// Refs: https://github.com/nodejs/node/issues/64555
70-
for (const dir of ['matching-patterns', 'matching-patterns/']) {
80+
for (const dir of ['matching-patterns', 'matching-patterns/',
81+
...literalDirs, ...literalDirs.map((dir) => `${dir}/`)]) {
7182
const args = ['--test', '--test-reporter=tap',
7283
'--no-experimental-strip-types',
7384
`--test-isolation=${isolation}`, dir];

0 commit comments

Comments
 (0)