diff --git a/lib/internal/test_runner/coverage.js b/lib/internal/test_runner/coverage.js index cdbece1eeae7c3..44c197d960c86a 100644 --- a/lib/internal/test_runner/coverage.js +++ b/lib/internal/test_runner/coverage.js @@ -46,6 +46,9 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?\d+ )?\*\//; const kLineEndingRegex = /\r?\n$/u; const kLineSplitRegex = /(?<=\r?\n)/u; const kStatusRegex = /\/\* node:coverage (?enable|disable) \*\//; +// Match dotfiles (e.g. `test/.foo.js`) when applying coverage globs so the +// default exclude patterns cover them. +const kMatchGlobPatternOptions = { __proto__: null, dot: true }; const kTypeOnlyImportRegex = /^\s*import\s+type\b/u; const kTypeScriptSourceRegex = /\.(?:cts|mts|ts)$/u; @@ -61,6 +64,14 @@ function getStripTypeScriptTypesForCoverage() { return stripTypeScriptTypesForCoverage; } +function createCoverageMatcher(pattern) { + return { + __proto__: null, + relative: createMatcher(pattern, kMatchGlobPatternOptions), + absolute: createMatcher(pattern), + }; +} + class CoverageLine { constructor(line, startOffset, src, length = src?.length) { const newlineLength = src == null ? 0 : @@ -557,23 +568,28 @@ class TestCoverage { // TestCoverage instance, so compile each glob to a matcher once and reuse // it for every file. Building a fresh Minimatch per call (the previous // behavior) dominated the coverage report time, scaling with - // files * globs. + // files * globs. Each glob compiles to a matcher pair: `relative` enables + // dot:true so globs match dotfiles within the project, while `absolute` + // keeps the default behavior to avoid misinterpreting dot segments in the + // absolute filesystem path (e.g. tmp dirs like `test/.tmp.0`). this.#excludeMatchers ??= ArrayPrototypeMap( - this.options.coverageExcludeGlobs ?? [], (pattern) => createMatcher(pattern)); + this.options.coverageExcludeGlobs ?? [], createCoverageMatcher); this.#includeMatchers ??= ArrayPrototypeMap( - this.options.coverageIncludeGlobs ?? [], (pattern) => createMatcher(pattern)); + this.options.coverageIncludeGlobs ?? [], createCoverageMatcher); // This check filters out files that match the exclude globs. for (let i = 0; i < this.#excludeMatchers.length; ++i) { const matcher = this.#excludeMatchers[i]; - if (matcher.match(relativePath) || matcher.match(absolutePath)) return true; + if (matcher.relative.match(relativePath) || + matcher.absolute.match(absolutePath)) return true; } // This check filters out files that do not match the include globs. if (this.#includeMatchers.length > 0) { for (let i = 0; i < this.#includeMatchers.length; ++i) { const matcher = this.#includeMatchers[i]; - if (matcher.match(relativePath) || matcher.match(absolutePath)) return false; + if (matcher.relative.match(relativePath) || + matcher.absolute.match(absolutePath)) 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..f6080612a37caa 100644 --- a/test/parallel/test-runner-coverage-default-exclusion.mjs +++ b/test/parallel/test-runner-coverage-default-exclusion.mjs @@ -16,6 +16,16 @@ async function setupFixtures() { await cp(fixtureDir, tmpdir.path, { recursive: true }); } +function assertDefaultExclusions(stdout) { + assert.match(stdout, /# start of coverage report/); + 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 +68,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 +80,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,7 +97,26 @@ 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 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(), ''); + assertDefaultExclusions(result.stdout.toString()); + assert.doesNotMatch(result.stdout.toString(), /#\s+\.dotfile\.cjs\s+\|/); assert.strictEqual(result.status, 0); }); });