Skip to content

Commit 8437db0

Browse files
committed
test_runner: match dotfiles in default coverage exclude
The default coverage exclude globs did not match dotfiles, so test files such as `test/.foo.test.js` were incorrectly included in coverage reports. Apply the `dot: true` minimatch option when matching the relative path so the default exclude patterns cover dotfiles, while keeping plain matching for the absolute path to avoid misinterpreting dot segments in the filesystem path (e.g. tmp dirs like `test/.tmp.0`). Fixes: #63397 Signed-off-by: semimikoh <ejffjeosms@gmail.com>
1 parent 0032189 commit 8437db0

3 files changed

Lines changed: 59 additions & 31 deletions

File tree

lib/internal/test_runner/coverage.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
4646
const kLineEndingRegex = /\r?\n$/u;
4747
const kLineSplitRegex = /(?<=\r?\n)/u;
4848
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
49+
// Match dotfiles (e.g. `test/.foo.js`) when applying coverage globs so the
50+
// default exclude patterns cover them.
51+
const kMatchGlobPatternOptions = { __proto__: null, dot: true };
4952
const kTypeOnlyImportRegex = /^\s*import\s+type\b/u;
5053
const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u;
5154

@@ -61,6 +64,14 @@ function getStripTypeScriptTypesForCoverage() {
6164
return stripTypeScriptTypesForCoverage;
6265
}
6366

67+
function createCoverageMatcher(pattern) {
68+
return {
69+
__proto__: null,
70+
relative: createMatcher(pattern, kMatchGlobPatternOptions),
71+
absolute: createMatcher(pattern),
72+
};
73+
}
74+
6475
class CoverageLine {
6576
constructor(line, startOffset, src, length = src?.length) {
6677
const newlineLength = src == null ? 0 :
@@ -557,23 +568,28 @@ class TestCoverage {
557568
// TestCoverage instance, so compile each glob to a matcher once and reuse
558569
// it for every file. Building a fresh Minimatch per call (the previous
559570
// behavior) dominated the coverage report time, scaling with
560-
// files * globs.
571+
// files * globs. Each glob compiles to a matcher pair: `relative` enables
572+
// dot:true so globs match dotfiles within the project, while `absolute`
573+
// keeps the default behavior to avoid misinterpreting dot segments in the
574+
// absolute filesystem path (e.g. tmp dirs like `test/.tmp.0`).
561575
this.#excludeMatchers ??= ArrayPrototypeMap(
562-
this.options.coverageExcludeGlobs ?? [], (pattern) => createMatcher(pattern));
576+
this.options.coverageExcludeGlobs ?? [], createCoverageMatcher);
563577
this.#includeMatchers ??= ArrayPrototypeMap(
564-
this.options.coverageIncludeGlobs ?? [], (pattern) => createMatcher(pattern));
578+
this.options.coverageIncludeGlobs ?? [], createCoverageMatcher);
565579

566580
// This check filters out files that match the exclude globs.
567581
for (let i = 0; i < this.#excludeMatchers.length; ++i) {
568582
const matcher = this.#excludeMatchers[i];
569-
if (matcher.match(relativePath) || matcher.match(absolutePath)) return true;
583+
if (matcher.relative.match(relativePath) ||
584+
matcher.absolute.match(absolutePath)) return true;
570585
}
571586

572587
// This check filters out files that do not match the include globs.
573588
if (this.#includeMatchers.length > 0) {
574589
for (let i = 0; i < this.#includeMatchers.length; ++i) {
575590
const matcher = this.#includeMatchers[i];
576-
if (matcher.match(relativePath) || matcher.match(absolutePath)) return false;
591+
if (matcher.relative.match(relativePath) ||
592+
matcher.absolute.match(absolutePath)) return false;
577593
}
578594
return true;
579595
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert');
3+
const { foo } = require('../logic-file.js');
4+
5+
test('foo returns 1 from a dotfile test', () => {
6+
assert.strictEqual(foo(), 1);
7+
});

test/parallel/test-runner-coverage-default-exclusion.mjs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ async function setupFixtures() {
1616
await cp(fixtureDir, tmpdir.path, { recursive: true });
1717
}
1818

19+
function assertDefaultExclusions(stdout) {
20+
assert.match(stdout, /# start of coverage report/);
21+
assert.doesNotMatch(stdout, /# file-test\.js\s+\|/);
22+
assert.doesNotMatch(stdout, /# file\.test\.mjs\s+\|/);
23+
assert.doesNotMatch(stdout, /# file\.test\.ts\s+\|/);
24+
assert.doesNotMatch(stdout, /# test\.cjs\s+\|/);
25+
assert.doesNotMatch(stdout, /#\s+not-matching-test-name\.js\s+\|/);
26+
assert.match(stdout, /# end of coverage report/);
27+
}
28+
1929
describe('test runner coverage default exclusion', skipIfNoInspector, () => {
2030
before(async () => {
2131
await setupFixtures();
@@ -58,18 +68,6 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => {
5868
});
5969

6070
it('should exclude test files from coverage by default', async () => {
61-
const report = [
62-
'# start of coverage report',
63-
'# --------------------------------------------------------------',
64-
'# file | line % | branch % | funcs % | uncovered lines',
65-
'# --------------------------------------------------------------',
66-
'# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7',
67-
'# --------------------------------------------------------------',
68-
'# all files | 66.67 | 100.00 | 50.00 | ',
69-
'# --------------------------------------------------------------',
70-
'# end of coverage report',
71-
].join('\n');
72-
7371
const args = [
7472
'--no-experimental-strip-types',
7573
'--test',
@@ -82,23 +80,11 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => {
8280
});
8381

8482
assert.strictEqual(result.stderr.toString(), '');
85-
assert(result.stdout.toString().includes(report));
83+
assertDefaultExclusions(result.stdout.toString());
8684
assert.strictEqual(result.status, 0);
8785
});
8886

8987
it('should exclude ts test files', async () => {
90-
const report = [
91-
'# start of coverage report',
92-
'# --------------------------------------------------------------',
93-
'# file | line % | branch % | funcs % | uncovered lines',
94-
'# --------------------------------------------------------------',
95-
'# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7',
96-
'# --------------------------------------------------------------',
97-
'# all files | 66.67 | 100.00 | 50.00 | ',
98-
'# --------------------------------------------------------------',
99-
'# end of coverage report',
100-
].join('\n');
101-
10288
const args = [
10389
'--test',
10490
'--experimental-test-coverage',
@@ -111,7 +97,26 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => {
11197
});
11298

11399
assert.strictEqual(result.stderr.toString(), '');
114-
assert(result.stdout.toString().includes(report));
100+
assertDefaultExclusions(result.stdout.toString());
101+
assert.strictEqual(result.status, 0);
102+
});
103+
104+
it('should exclude dotfile test files from coverage by default', async () => {
105+
const args = [
106+
'--no-experimental-strip-types',
107+
'--test',
108+
'--experimental-test-coverage',
109+
'--test-reporter=tap',
110+
'test/.dotfile.cjs',
111+
];
112+
const result = spawnSync(process.execPath, args, {
113+
env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path },
114+
cwd: tmpdir.path
115+
});
116+
117+
assert.strictEqual(result.stderr.toString(), '');
118+
assertDefaultExclusions(result.stdout.toString());
119+
assert.doesNotMatch(result.stdout.toString(), /#\s+\.dotfile\.cjs\s+\|/);
115120
assert.strictEqual(result.status, 0);
116121
});
117122
});

0 commit comments

Comments
 (0)