Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
const kLineEndingRegex = /\r?\n$/u;
const kLineSplitRegex = /(?<=\r?\n)/u;
const kStatusRegex = /\/\* node:coverage (?<status>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;

Expand All @@ -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 :
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
57 changes: 31 additions & 26 deletions test/parallel/test-runner-coverage-default-exclusion.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand All @@ -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);
});
});
Loading