From 0ca5673029e4c781a8e944d9abda7abf811750cc Mon Sep 17 00:00:00 2001 From: semimikoh Date: Mon, 18 May 2026 13:26:09 +0900 Subject: [PATCH 1/5] test_runner: match dotfiles in default coverage exclude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default coverage exclude patterns ... (본문) Fixes: https://github.com/nodejs/node/issues/63397 Signed-off-by: semimikoh --- lib/internal/fs/glob.js | 4 ++- lib/internal/test_runner/coverage.js | 13 +++++--- .../test/.dotfile.cjs | 7 +++++ ...test-runner-coverage-default-exclusion.mjs | 30 +++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index c5bbdb9813c0d1..01d331d3f0c39e 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -927,13 +927,15 @@ class Glob { * @param {string} path the path to check * @param {string} pattern the glob pattern to match * @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows` + * @param {object} [options] the options for the minimatch module * @returns {boolean} */ -function matchGlobPattern(path, pattern, windows = isWindows) { +function matchGlobPattern(path, pattern, windows = isWindows, options = kEmptyObject) { validateString(path, 'path'); validateString(pattern, 'pattern'); return lazyMinimatch().minimatch(path, pattern, { kEmptyObject, + ...options, nocase: isMacOS || isWindows, windowsPathsNoEscape: true, nonegate: true, diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..4b964b7ed50a58 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -44,6 +44,11 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +const kMatchGlobPatternOptions = { __proto__: null, dot: true }; + +function matchCoverageGlob(path, pattern) { + return matchGlobPattern(path, pattern, undefined, kMatchGlobPatternOptions); +} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -481,8 +486,8 @@ class TestCoverage { if (excludeGlobs?.length > 0) { for (let i = 0; i < excludeGlobs.length; ++i) { if ( - matchGlobPattern(relativePath, excludeGlobs[i]) || - matchGlobPattern(absolutePath, excludeGlobs[i]) + matchCoverageGlob(relativePath, excludeGlobs[i]) || + matchCoverageGlob(absolutePath, excludeGlobs[i]) ) return true; } } @@ -491,8 +496,8 @@ class TestCoverage { if (includeGlobs?.length > 0) { for (let i = 0; i < includeGlobs.length; ++i) { if ( - matchGlobPattern(relativePath, includeGlobs[i]) || - matchGlobPattern(absolutePath, includeGlobs[i]) + matchCoverageGlob(relativePath, includeGlobs[i]) || + matchCoverageGlob(absolutePath, includeGlobs[i]) ) return false; } return true; diff --git a/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs b/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs new file mode 100644 index 00000000000000..ec0a4c24fffb3a --- /dev/null +++ b/test/fixtures/test-runner/coverage-default-exclusion/test/.dotfile.cjs @@ -0,0 +1,7 @@ +const test = require('node:test'); +const assert = require('node:assert'); +const { foo } = require('../logic-file.js'); + +test('foo returns 1 from a dotfile test', () => { + assert.strictEqual(foo(), 1); +}); diff --git a/test/parallel/test-runner-coverage-default-exclusion.mjs b/test/parallel/test-runner-coverage-default-exclusion.mjs index 44e5f7600d3270..2ef6d81bf1dd9b 100644 --- a/test/parallel/test-runner-coverage-default-exclusion.mjs +++ b/test/parallel/test-runner-coverage-default-exclusion.mjs @@ -114,4 +114,34 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { assert(result.stdout.toString().includes(report)); assert.strictEqual(result.status, 0); }); + + it('should exclude dotfile test files from coverage by default', async () => { + const report = [ + '# start of coverage report', + '# --------------------------------------------------------------', + '# file | line % | branch % | funcs % | uncovered lines', + '# --------------------------------------------------------------', + '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', + '# --------------------------------------------------------------', + '# all files | 66.67 | 100.00 | 50.00 | ', + '# --------------------------------------------------------------', + '# end of coverage report', + ].join('\n'); + + const args = [ + '--no-experimental-strip-types', + '--test', + '--experimental-test-coverage', + '--test-reporter=tap', + 'test/.dotfile.cjs', + ]; + const result = spawnSync(process.execPath, args, { + env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path }, + cwd: tmpdir.path + }); + + assert.strictEqual(result.stderr.toString(), ''); + assert(result.stdout.toString().includes(report)); + assert.strictEqual(result.status, 0); + }); }); From c241614599dafd87c656384043159acca01a7eeb Mon Sep 17 00:00:00 2001 From: semimikoh Date: Mon, 18 May 2026 15:03:35 +0900 Subject: [PATCH 2/5] fixup! test_runner: match dotfiles in default coverage exclude Signed-off-by: semimikoh --- lib/internal/fs/glob.js | 4 +--- lib/internal/test_runner/coverage.js | 13 ++++--------- lib/internal/test_runner/utils.js | 4 +++- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 01d331d3f0c39e..c5bbdb9813c0d1 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -927,15 +927,13 @@ class Glob { * @param {string} path the path to check * @param {string} pattern the glob pattern to match * @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows` - * @param {object} [options] the options for the minimatch module * @returns {boolean} */ -function matchGlobPattern(path, pattern, windows = isWindows, options = kEmptyObject) { +function matchGlobPattern(path, pattern, windows = isWindows) { validateString(path, 'path'); validateString(pattern, 'pattern'); return lazyMinimatch().minimatch(path, pattern, { kEmptyObject, - ...options, nocase: isMacOS || isWindows, windowsPathsNoEscape: true, nonegate: true, diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 4b964b7ed50a58..8fa9c872568d1e 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -44,11 +44,6 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; -const kMatchGlobPatternOptions = { __proto__: null, dot: true }; - -function matchCoverageGlob(path, pattern) { - return matchGlobPattern(path, pattern, undefined, kMatchGlobPatternOptions); -} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -486,8 +481,8 @@ class TestCoverage { if (excludeGlobs?.length > 0) { for (let i = 0; i < excludeGlobs.length; ++i) { if ( - matchCoverageGlob(relativePath, excludeGlobs[i]) || - matchCoverageGlob(absolutePath, excludeGlobs[i]) + matchGlobPattern(relativePath, excludeGlobs[i]) || + matchGlobPattern(absolutePath, excludeGlobs[i]) ) return true; } } @@ -496,8 +491,8 @@ class TestCoverage { if (includeGlobs?.length > 0) { for (let i = 0; i < includeGlobs.length; ++i) { if ( - matchCoverageGlob(relativePath, includeGlobs[i]) || - matchCoverageGlob(absolutePath, includeGlobs[i]) + matchGlobPattern(relativePath, includeGlobs[i]) || + matchGlobPattern(absolutePath, includeGlobs[i]) ) return false; } return true; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 5a29673279311b..463ea81c44d5f8 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -75,6 +75,8 @@ if (getOptionValue('--strip-types')) { ArrayPrototypePush(kFileExtensions, 'ts', 'mts', 'cts'); } const kDefaultPattern = `**/{${ArrayPrototypeJoin(kPatterns, ',')}}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`; +const kDefaultCoverageDotfilePattern = + `test/{.*,**/.*}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`; function createDeferredCallback() { let calledCount = 0; @@ -403,7 +405,7 @@ function parseCommandLine() { if (!coverageExcludeGlobs || coverageExcludeGlobs.length === 0) { // TODO(pmarchini): this default should follow something similar to c8 defaults // Default exclusions should be also exported to be used by other tools / users - coverageExcludeGlobs = [kDefaultPattern]; + coverageExcludeGlobs = [kDefaultPattern, kDefaultCoverageDotfilePattern]; } coverageIncludeGlobs = getOptionValue('--test-coverage-include'); From 020e62ede3aac8be29c71c00e554f2d60b06b5eb Mon Sep 17 00:00:00 2001 From: semimikoh Date: Tue, 19 May 2026 08:40:34 +0900 Subject: [PATCH 3/5] fixup! test_runner: match dotfiles in default coverage exclude Signed-off-by: semimikoh --- lib/internal/fs/glob.js | 4 +++- lib/internal/test_runner/coverage.js | 13 +++++++++---- lib/internal/test_runner/utils.js | 4 +--- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index c5bbdb9813c0d1..01d331d3f0c39e 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -927,13 +927,15 @@ class Glob { * @param {string} path the path to check * @param {string} pattern the glob pattern to match * @param {boolean} windows whether the path is on a Windows system, defaults to `isWindows` + * @param {object} [options] the options for the minimatch module * @returns {boolean} */ -function matchGlobPattern(path, pattern, windows = isWindows) { +function matchGlobPattern(path, pattern, windows = isWindows, options = kEmptyObject) { validateString(path, 'path'); validateString(pattern, 'pattern'); return lazyMinimatch().minimatch(path, pattern, { kEmptyObject, + ...options, nocase: isMacOS || isWindows, windowsPathsNoEscape: true, nonegate: true, diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 8fa9c872568d1e..4b964b7ed50a58 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -44,6 +44,11 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +const kMatchGlobPatternOptions = { __proto__: null, dot: true }; + +function matchCoverageGlob(path, pattern) { + return matchGlobPattern(path, pattern, undefined, kMatchGlobPatternOptions); +} class CoverageLine { constructor(line, startOffset, src, length = src?.length) { @@ -481,8 +486,8 @@ class TestCoverage { if (excludeGlobs?.length > 0) { for (let i = 0; i < excludeGlobs.length; ++i) { if ( - matchGlobPattern(relativePath, excludeGlobs[i]) || - matchGlobPattern(absolutePath, excludeGlobs[i]) + matchCoverageGlob(relativePath, excludeGlobs[i]) || + matchCoverageGlob(absolutePath, excludeGlobs[i]) ) return true; } } @@ -491,8 +496,8 @@ class TestCoverage { if (includeGlobs?.length > 0) { for (let i = 0; i < includeGlobs.length; ++i) { if ( - matchGlobPattern(relativePath, includeGlobs[i]) || - matchGlobPattern(absolutePath, includeGlobs[i]) + matchCoverageGlob(relativePath, includeGlobs[i]) || + matchCoverageGlob(absolutePath, includeGlobs[i]) ) return false; } return true; diff --git a/lib/internal/test_runner/utils.js b/lib/internal/test_runner/utils.js index 463ea81c44d5f8..5a29673279311b 100644 --- a/lib/internal/test_runner/utils.js +++ b/lib/internal/test_runner/utils.js @@ -75,8 +75,6 @@ if (getOptionValue('--strip-types')) { ArrayPrototypePush(kFileExtensions, 'ts', 'mts', 'cts'); } const kDefaultPattern = `**/{${ArrayPrototypeJoin(kPatterns, ',')}}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`; -const kDefaultCoverageDotfilePattern = - `test/{.*,**/.*}.{${ArrayPrototypeJoin(kFileExtensions, ',')}}`; function createDeferredCallback() { let calledCount = 0; @@ -405,7 +403,7 @@ function parseCommandLine() { if (!coverageExcludeGlobs || coverageExcludeGlobs.length === 0) { // TODO(pmarchini): this default should follow something similar to c8 defaults // Default exclusions should be also exported to be used by other tools / users - coverageExcludeGlobs = [kDefaultPattern, kDefaultCoverageDotfilePattern]; + coverageExcludeGlobs = [kDefaultPattern]; } coverageIncludeGlobs = getOptionValue('--test-coverage-include'); From c72d1ebf0639aa2e27b6ca16c071f151d5119390 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Tue, 19 May 2026 15:15:40 +0900 Subject: [PATCH 4/5] fixup! test_runner: match dotfiles in default coverage exclude Signed-off-by: semimikoh --- ...test-runner-coverage-default-exclusion.mjs | 54 ++++++------------- 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/test/parallel/test-runner-coverage-default-exclusion.mjs b/test/parallel/test-runner-coverage-default-exclusion.mjs index 2ef6d81bf1dd9b..22eb333b10007b 100644 --- a/test/parallel/test-runner-coverage-default-exclusion.mjs +++ b/test/parallel/test-runner-coverage-default-exclusion.mjs @@ -16,6 +16,17 @@ async function setupFixtures() { await cp(fixtureDir, tmpdir.path, { recursive: true }); } +function assertDefaultExclusions(stdout) { + assert.match(stdout, /# start of coverage report/); + assert.match(stdout, /# logic-file\.js\s+\|/); + assert.doesNotMatch(stdout, /# file-test\.js\s+\|/); + assert.doesNotMatch(stdout, /# file\.test\.mjs\s+\|/); + assert.doesNotMatch(stdout, /# file\.test\.ts\s+\|/); + assert.doesNotMatch(stdout, /# test\.cjs\s+\|/); + assert.doesNotMatch(stdout, /#\s+not-matching-test-name\.js\s+\|/); + assert.match(stdout, /# end of coverage report/); +} + describe('test runner coverage default exclusion', skipIfNoInspector, () => { before(async () => { await setupFixtures(); @@ -58,18 +69,6 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); it('should exclude test files from coverage by default', async () => { - const report = [ - '# start of coverage report', - '# --------------------------------------------------------------', - '# file | line % | branch % | funcs % | uncovered lines', - '# --------------------------------------------------------------', - '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', - '# --------------------------------------------------------------', - '# all files | 66.67 | 100.00 | 50.00 | ', - '# --------------------------------------------------------------', - '# end of coverage report', - ].join('\n'); - const args = [ '--no-experimental-strip-types', '--test', @@ -82,23 +81,11 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertDefaultExclusions(result.stdout.toString()); assert.strictEqual(result.status, 0); }); it('should exclude ts test files', async () => { - const report = [ - '# start of coverage report', - '# --------------------------------------------------------------', - '# file | line % | branch % | funcs % | uncovered lines', - '# --------------------------------------------------------------', - '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', - '# --------------------------------------------------------------', - '# all files | 66.67 | 100.00 | 50.00 | ', - '# --------------------------------------------------------------', - '# end of coverage report', - ].join('\n'); - const args = [ '--test', '--experimental-test-coverage', @@ -111,23 +98,11 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertDefaultExclusions(result.stdout.toString()); assert.strictEqual(result.status, 0); }); it('should exclude dotfile test files from coverage by default', async () => { - const report = [ - '# start of coverage report', - '# --------------------------------------------------------------', - '# file | line % | branch % | funcs % | uncovered lines', - '# --------------------------------------------------------------', - '# logic-file.js | 66.67 | 100.00 | 50.00 | 5-7', - '# --------------------------------------------------------------', - '# all files | 66.67 | 100.00 | 50.00 | ', - '# --------------------------------------------------------------', - '# end of coverage report', - ].join('\n'); - const args = [ '--no-experimental-strip-types', '--test', @@ -141,7 +116,8 @@ describe('test runner coverage default exclusion', skipIfNoInspector, () => { }); assert.strictEqual(result.stderr.toString(), ''); - assert(result.stdout.toString().includes(report)); + assertDefaultExclusions(result.stdout.toString()); + assert.doesNotMatch(result.stdout.toString(), /#\s+\.dotfile\.cjs\s+\|/); assert.strictEqual(result.status, 0); }); }); From b0e1f2ca002c32a8af710d9011220b1811edd971 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Tue, 19 May 2026 17:01:46 +0900 Subject: [PATCH 5/5] fixup! test_runner: match dotfiles in default coverage exclude Signed-off-by: semimikoh --- lib/internal/test_runner/coverage.js | 7 +++++-- test/parallel/test-runner-coverage-default-exclusion.mjs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index 4b964b7ed50a58..6e862c3fff1f59 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -483,11 +483,14 @@ class TestCoverage { } = this.options; // This check filters out files that match the exclude globs. + // dot:true is only applied to relativePath so user-supplied globs match + // dotfiles within the project, without misinterpreting dot segments in the + // absolute filesystem path (e.g. test tmp dirs like `test/.tmp.0`). if (excludeGlobs?.length > 0) { for (let i = 0; i < excludeGlobs.length; ++i) { if ( matchCoverageGlob(relativePath, excludeGlobs[i]) || - matchCoverageGlob(absolutePath, excludeGlobs[i]) + matchGlobPattern(absolutePath, excludeGlobs[i]) ) return true; } } @@ -497,7 +500,7 @@ class TestCoverage { for (let i = 0; i < includeGlobs.length; ++i) { if ( matchCoverageGlob(relativePath, includeGlobs[i]) || - matchCoverageGlob(absolutePath, includeGlobs[i]) + matchGlobPattern(absolutePath, includeGlobs[i]) ) return false; } return true; diff --git a/test/parallel/test-runner-coverage-default-exclusion.mjs b/test/parallel/test-runner-coverage-default-exclusion.mjs index 22eb333b10007b..f6080612a37caa 100644 --- a/test/parallel/test-runner-coverage-default-exclusion.mjs +++ b/test/parallel/test-runner-coverage-default-exclusion.mjs @@ -18,7 +18,6 @@ async function setupFixtures() { function assertDefaultExclusions(stdout) { assert.match(stdout, /# start of coverage report/); - assert.match(stdout, /# logic-file\.js\s+\|/); assert.doesNotMatch(stdout, /# file-test\.js\s+\|/); assert.doesNotMatch(stdout, /# file\.test\.mjs\s+\|/); assert.doesNotMatch(stdout, /# file\.test\.ts\s+\|/);