Skip to content

Commit

Permalink
test_runner: exclude test files from code coverage by default
Browse files Browse the repository at this point in the history
Is not usual to test the code coverage of test files.

Fixes: nodejs#53508
  • Loading branch information
Llorx committed Nov 1, 2024
1 parent 824c149 commit 37391f4
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 27 deletions.
2 changes: 2 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2324,6 +2324,8 @@ This option may be specified multiple times to exclude multiple glob patterns.
If both `--test-coverage-exclude` and `--test-coverage-include` are provided,
files must meet **both** criteria to be included in the coverage report.

If no `--test-coverage-exclude` is provided, all test files will be excluded.

### `--test-coverage-functions=threshold`

<!-- YAML
Expand Down
1 change: 1 addition & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,7 @@ changes:
This property is only applicable when `coverage` was set to `true`.
If both `coverageExcludeGlobs` and `coverageIncludeGlobs` are provided,
files must meet **both** criteria to be included in the coverage report.
If `undefined`, all test files will be excluded.
**Default:** `undefined`.
* `coverageIncludeGlobs` {string|Array} Includes specific files in code coverage
using a glob pattern, which can match both absolute and relative file paths.
Expand Down
22 changes: 12 additions & 10 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ class TestCoverage {
return lines;
}

summary() {
summary(excludeFiles) {
internalBinding('profiler').takeCoverage();
const coverage = this.getCoverageFromDirectory();
const coverage = this.getCoverageFromDirectory(excludeFiles);
const coverageSummary = {
__proto__: null,
workingDirectory: this.workingDirectory,
Expand Down Expand Up @@ -319,7 +319,7 @@ class TestCoverage {
}
}

getCoverageFromDirectory() {
getCoverageFromDirectory(excludeFiles) {
const result = new SafeMap();
let dir;

Expand All @@ -333,7 +333,7 @@ class TestCoverage {

const coverageFile = join(this.coverageDirectory, entry.name);
const coverage = JSONParse(readFileSync(coverageFile, 'utf8'));
this.mergeCoverage(result, this.mapCoverageWithSourceMap(coverage));
this.mergeCoverage(result, this.mapCoverageWithSourceMap(coverage, excludeFiles), excludeFiles);
}

return ArrayFrom(result.values());
Expand All @@ -345,7 +345,7 @@ class TestCoverage {
}


mapCoverageWithSourceMap(coverage) {
mapCoverageWithSourceMap(coverage, excludeFiles) {
const { result } = coverage;
const sourceMapCache = coverage['source-map-cache'];
if (!this.sourceMaps || !sourceMapCache) {
Expand All @@ -356,7 +356,7 @@ class TestCoverage {
const script = result[i];
const { url, functions } = script;

if (this.shouldSkipFileCoverage(url) || sourceMapCache[url] == null) {
if (this.shouldSkipFileCoverage(url, excludeFiles) || sourceMapCache[url] == null) {
newResult.set(url, script);
continue;
}
Expand Down Expand Up @@ -436,12 +436,12 @@ class TestCoverage {
return MathMin(lines[line].startOffset + entry.originalColumn, lines[line].endOffset);
}

mergeCoverage(merged, coverage) {
mergeCoverage(merged, coverage, excludeFiles) {
for (let i = 0; i < coverage.length; ++i) {
const newScript = coverage[i];
const { url } = newScript;

if (this.shouldSkipFileCoverage(url)) {
if (this.shouldSkipFileCoverage(url, excludeFiles)) {
continue;
}

Expand All @@ -455,7 +455,7 @@ class TestCoverage {
}
}

shouldSkipFileCoverage(url) {
shouldSkipFileCoverage(url, excludeFiles) {
// This check filters out core modules, which start with 'node:' in
// coverage reports, as well as any invalid coverages which have been
// observed on Windows.
Expand All @@ -465,11 +465,13 @@ class TestCoverage {
const relativePath = relative(this.workingDirectory, absolutePath);

// This check filters out files that match the exclude globs.
if (this.excludeGlobs?.length > 0) {
if (this.excludeGlobs) {
for (let i = 0; i < this.excludeGlobs.length; ++i) {
if (matchesGlob(relativePath, this.excludeGlobs[i]) ||
matchesGlob(absolutePath, this.excludeGlobs[i])) return true;
}
} else if (excludeFiles?.includes(absolutePath)) {
return true;
}

// This check filters out files that do not match the include globs.
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function collectCoverage(rootTest, coverage) {
let summary = null;

try {
summary = coverage.summary();
summary = coverage.summary(rootTest.getFiles());
} catch (err) {
rootTest.diagnostic(`Warning: Could not report code coverage. ${err}`);
rootTest.harness.success = false;
Expand Down
1 change: 1 addition & 0 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class FileTest extends Test {

constructor(options) {
super(options);
this.filePath = resolve(this.config.cwd, this.name);
this.loc ??= {
__proto__: null,
line: 1,
Expand Down
14 changes: 13 additions & 1 deletion lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -998,7 +998,19 @@ class Test extends AsyncResource {
process.exit();
}
}

getFiles() {
const files = [];
if (this.filePath) {
files.push(this.filePath);
}
if (this.subtests) {
for (const test of this.subtests) {
const subFiles = test.getFiles();
files.push(...subFiles);
}
}
return files;
}
postRun(pendingSubtestsError) {
// If the test was cancelled before it started, then the start and end
// times need to be corrected.
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-runner-coverage-source-map.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function generateReport(report) {

const flags = [
'--enable-source-maps',
'--test', '--experimental-test-coverage', '--test-reporter', 'tap',
'--test', '--experimental-test-coverage', '--no-warnings', '--test-coverage-include=**', '--test-reporter', 'tap',
];

describe('Coverage with source maps', async () => {
Expand Down
12 changes: 12 additions & 0 deletions test/parallel/test-runner-coverage-thresholds.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=25`,
'--test-reporter', 'tap',
fixture,
Expand All @@ -77,6 +79,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=25`,
'--test-reporter', reporter,
fixture,
Expand All @@ -92,6 +96,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=99`,
'--test-reporter', 'tap',
fixture,
Expand All @@ -108,6 +114,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=99`,
'--test-reporter', reporter,
fixture,
Expand All @@ -123,6 +131,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=101`,
fixture,
]);
Expand All @@ -136,6 +146,8 @@ for (const coverage of coverages) {
const result = spawnSync(process.execPath, [
'--test',
'--experimental-test-coverage',
'--no-warnings',
'--test-coverage-include=**',
`${coverage.flag}=-1`,
fixture,
]);
Expand Down
53 changes: 42 additions & 11 deletions test/parallel/test-runner-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ test('test spec coverage reporter', skipIfNoInspector, async (t) => {
test('single process coverage is the same with --test', skipIfNoInspector, () => {
const fixture = fixtures.path('test-runner', 'coverage.js');
const args = [
'--test', '--experimental-test-coverage', '--test-reporter', 'tap', fixture,
'--test', '--experimental-test-coverage',
'--no-warnings', '--test-coverage-include=**',
'--test-reporter', 'tap', fixture,
];
const result = spawnSync(process.execPath, args);
const report = getTapCoverageFixtureReport();
Expand Down Expand Up @@ -183,7 +185,7 @@ test('coverage is combined for multiple processes', skipIfNoInspector, () => {

const fixture = fixtures.path('v8-coverage', 'combined_coverage');
const args = [
'--test', '--experimental-test-coverage', '--test-reporter', 'tap',
'--test', '--experimental-test-coverage', '--no-warnings', '--test-coverage-include=**', '--test-reporter', 'tap',
];
const result = spawnSync(process.execPath, args, {
env: { ...process.env, NODE_TEST_TMPDIR: tmpdir.path },
Expand Down Expand Up @@ -236,7 +238,8 @@ test.skip('coverage works with isolation=none', skipIfNoInspector, () => {
test('coverage reports on lines, functions, and branches', skipIfNoInspector, async (t) => {
const fixture = fixtures.path('test-runner', 'coverage.js');
const child = spawnSync(process.execPath,
['--test', '--experimental-test-coverage', '--test-reporter',
['--test', '--experimental-test-coverage',
'--no-warnings', '--test-coverage-include=**', '--test-reporter',
fixtures.fileURL('test-runner/custom_reporters/coverage.mjs'),
fixture]);
assert.strictEqual(child.stderr.toString(), '');
Expand Down Expand Up @@ -297,7 +300,6 @@ test('coverage with ESM hook - source irrelevant', skipIfNoInspector, () => {
'# ------------------------------------------------------------------',
'# hooks.mjs | 100.00 | 100.00 | 100.00 | ',
'# register-hooks.js | 100.00 | 100.00 | 100.00 | ',
'# virtual.js | 100.00 | 100.00 | 100.00 | ',
'# ------------------------------------------------------------------',
'# all files | 100.00 | 100.00 | 100.00 | ',
'# ------------------------------------------------------------------',
Expand Down Expand Up @@ -327,7 +329,6 @@ test('coverage with ESM hook - source transpiled', skipIfNoInspector, () => {
'# ------------------------------------------------------------------',
'# hooks.mjs | 100.00 | 100.00 | 100.00 | ',
'# register-hooks.js | 100.00 | 100.00 | 100.00 | ',
'# sum.test.ts | 100.00 | 100.00 | 100.00 | ',
'# sum.ts | 100.00 | 100.00 | 100.00 | ',
'# ------------------------------------------------------------------',
'# all files | 100.00 | 100.00 | 100.00 | ',
Expand Down Expand Up @@ -384,7 +385,38 @@ test('coverage with excluded files', skipIfNoInspector, () => {
assert.strictEqual(result.status, 0);
assert(!findCoverageFileForPid(result.pid));
});
test('coverage should not include test files by default', skipIfNoInspector, () => {
const fixture = fixtures.path('test-runner', 'coverage.js');
const args = [
'--experimental-test-coverage', '--test-reporter', 'tap',
fixture,
];
const result = spawnSync(process.execPath, args);
const report = [
'# start of coverage report',
'# ---------------------------------------------------------------',
'# file | line % | branch % | funcs % | uncovered lines',
'# ---------------------------------------------------------------',
'# test | | | | ',
'# fixtures | | | | ',
'# test-runner | | | | ',
'# v8-coverage | | | | ',
'# throw.js | 71.43 | 50.00 | 100.00 | 5-6',
'# ---------------------------------------------------------------',
'# all files | 78.13 | 40.00 | 60.00 | ',
'# ---------------------------------------------------------------',
'# end of coverage report',
].join('\n');


if (common.isWindows) {
return report.replaceAll('/', '\\');
}

assert(result.stdout.toString().includes(report));
assert.strictEqual(result.status, 0);
assert(!findCoverageFileForPid(result.pid));
});
test('coverage with included files', skipIfNoInspector, () => {
const fixture = fixtures.path('test-runner', 'coverage.js');
const args = [
Expand Down Expand Up @@ -458,18 +490,17 @@ test('coverage with included and excluded files', skipIfNoInspector, () => {
test('correctly prints the coverage report of files contained in parent directories', skipIfNoInspector, () => {
let report = [
'# start of coverage report',
'# --------------------------------------------------------------------------------------------',
'# ------------------------------------------------------------------',
'# file | line % | branch % | funcs % | uncovered lines',
'# --------------------------------------------------------------------------------------------',
'# ------------------------------------------------------------------',
'# .. | | | | ',
'# coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72',
'# invalid-tap.js | 100.00 | 100.00 | 100.00 | ',
'# .. | | | | ',
'# v8-coverage | | | | ',
'# throw.js | 71.43 | 50.00 | 100.00 | 5-6',
'# --------------------------------------------------------------------------------------------',
'# all files | 78.35 | 43.75 | 60.00 | ',
'# --------------------------------------------------------------------------------------------',
'# ------------------------------------------------------------------',
'# all files | 75.00 | 66.67 | 100.00 | ',
'# ------------------------------------------------------------------',
'# end of coverage report',
].join('\n');

Expand Down
7 changes: 4 additions & 3 deletions test/parallel/test-runner-run-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ describe('require(\'node:test\').run coverage settings', { concurrency: true },

await it('should run while including and excluding globs', async () => {
const stream = run({
files: [...files, fixtures.path('test-runner/invalid-tap.js')],
files: files,
coverage: true,
coverageIncludeGlobs: ['test/fixtures/test-runner/*.js'],
coverageExcludeGlobs: ['test/fixtures/test-runner/*-tap.js']
});
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(2));
stream.on('test:pass', common.mustCall(1));
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
const filesPaths = files.map(({ path }) => path);
assert.strictEqual(filesPaths.every((path) => !path.includes(`test-runner${sep}invalid-tap.js`)), true);
Expand All @@ -157,7 +157,8 @@ describe('require(\'node:test\').run coverage settings', { concurrency: true },
const thresholdErrors = [];
const originalExitCode = process.exitCode;
assert.notStrictEqual(originalExitCode, 1);
const stream = run({ files, coverage: true, lineCoverage: 99, branchCoverage: 99, functionCoverage: 99 });
const stream = run({ files, coverageIncludeGlobs: '**', coverage: true,
lineCoverage: 99, branchCoverage: 99, functionCoverage: 99 });
stream.on('test:fail', common.mustNotCall());
stream.on('test:pass', common.mustCall(1));
stream.on('test:diagnostic', ({ message }) => {
Expand Down

0 comments on commit 37391f4

Please sign in to comment.