Skip to content

Commit 3a0fcbb

Browse files
avivkelleraduh95
authored andcommitted
test_runner: support glob matching coverage files
PR-URL: #53553 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 1a7c2dc commit 3a0fcbb

File tree

7 files changed

+179
-16
lines changed

7 files changed

+179
-16
lines changed

doc/api/cli.md

+36
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,40 @@ For example, to run a module with "development" resolutions:
490490
node -C development app.js
491491
```
492492

493+
### `--test-coverage-exclude`
494+
495+
<!-- YAML
496+
added:
497+
- REPLACEME
498+
-->
499+
500+
> Stability: 1 - Experimental
501+
502+
Excludes specific files from code coverage using a glob pattern, which can match
503+
both absolute and relative file paths.
504+
505+
This option may be specified multiple times to exclude multiple glob patterns.
506+
507+
If both `--test-coverage-exclude` and `--test-coverage-include` are provided,
508+
files must meet **both** criteria to be included in the coverage report.
509+
510+
### `--test-coverage-include`
511+
512+
<!-- YAML
513+
added:
514+
- REPLACEME
515+
-->
516+
517+
> Stability: 1 - Experimental
518+
519+
Includes specific files in code coverage using a glob pattern, which can match
520+
both absolute and relative file paths.
521+
522+
This option may be specified multiple times to include multiple glob patterns.
523+
524+
If both `--test-coverage-exclude` and `--test-coverage-include` are provided,
525+
files must meet **both** criteria to be included in the coverage report.
526+
493527
### `--cpu-prof`
494528

495529
<!-- YAML
@@ -2961,6 +2995,8 @@ one is included in the list below.
29612995
* `--secure-heap-min`
29622996
* `--secure-heap`
29632997
* `--snapshot-blob`
2998+
* `--test-coverage-exclude`
2999+
* `--test-coverage-include`
29643000
* `--test-only`
29653001
* `--test-reporter-destination`
29663002
* `--test-reporter`

doc/api/test.md

-5
Original file line numberDiff line numberDiff line change
@@ -511,11 +511,6 @@ node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-de
511511
* No test results are reported by this reporter.
512512
* This reporter should ideally be used alongside another reporter.
513513

514-
### Limitations
515-
516-
The test runner's code coverage functionality does not support excluding
517-
specific files or directories from the coverage report.
518-
519514
## Mocking
520515

521516
The `node:test` module supports mocking during testing via a top-level `mock`

doc/node.1

+6
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ Starts the Node.js command line test runner.
444444
The maximum number of test files that the test runner CLI will execute
445445
concurrently.
446446
.
447+
.It Fl -test-coverage-exclude
448+
A glob pattern that excludes matching files from the coverage report
449+
.
450+
.It Fl -test-coverage-include
451+
A glob pattern that only includes matching files in the coverage report
452+
.
447453
.It Fl -test-force-exit
448454
Configures the test runner to exit the process once all known tests have
449455
finished executing even if the event loop would otherwise remain active.

lib/internal/test_runner/coverage.js

+34-11
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@ const {
2525
readFileSync,
2626
} = require('fs');
2727
const { setupCoverageHooks } = require('internal/util');
28+
const { getOptionValue } = require('internal/options');
2829
const { tmpdir } = require('os');
29-
const { join, resolve } = require('path');
30+
const { join, resolve, relative, matchesGlob } = require('path');
3031
const { fileURLToPath } = require('internal/url');
3132
const { kMappings, SourceMap } = require('internal/source_map/source_map');
3233
const kCoverageFileRegex = /^coverage-(\d+)-(\d{13})-(\d+)\.json$/;
3334
const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
3435
const kLineEndingRegex = /\r?\n$/u;
3536
const kLineSplitRegex = /(?<=\r?\n)/u;
3637
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
38+
const excludeFileGlobs = getOptionValue('--test-coverage-exclude');
39+
const includeFileGlobs = getOptionValue('--test-coverage-include');
3740

3841
class CoverageLine {
3942
constructor(line, startOffset, src, length = src?.length) {
@@ -308,7 +311,7 @@ class TestCoverage {
308311

309312
const coverageFile = join(this.coverageDirectory, entry.name);
310313
const coverage = JSONParse(readFileSync(coverageFile, 'utf8'));
311-
mergeCoverage(result, this.mapCoverageWithSourceMap(coverage));
314+
mergeCoverage(result, this.mapCoverageWithSourceMap(coverage), this.workingDirectory);
312315
}
313316

314317
return ArrayFrom(result.values());
@@ -331,7 +334,7 @@ class TestCoverage {
331334
const script = result[i];
332335
const { url, functions } = script;
333336

334-
if (shouldSkipFileCoverage(url) || sourceMapCache[url] == null) {
337+
if (shouldSkipFileCoverage(url, this.workingDirectory) || sourceMapCache[url] == null) {
335338
newResult.set(url, script);
336339
continue;
337340
}
@@ -485,22 +488,42 @@ function mapRangeToLines(range, lines) {
485488
return { __proto__: null, lines: mappedLines, ignoredLines };
486489
}
487490

488-
function shouldSkipFileCoverage(url) {
489-
// The first part of this check filters out the node_modules/ directory
490-
// from the results. This filter is applied first because most real world
491-
// applications will be dominated by third party dependencies. The second
492-
// part of the check filters out core modules, which start with 'node:' in
491+
function shouldSkipFileCoverage(url, workingDirectory) {
492+
// This check filters out core modules, which start with 'node:' in
493493
// coverage reports, as well as any invalid coverages which have been
494494
// observed on Windows.
495-
return StringPrototypeIncludes(url, '/node_modules/') || !StringPrototypeStartsWith(url, 'file:');
495+
if (!StringPrototypeStartsWith(url, 'file:')) return true;
496+
497+
const absolutePath = fileURLToPath(url);
498+
const relativePath = relative(workingDirectory, absolutePath);
499+
500+
// This check filters out files that match the exclude globs.
501+
if (excludeFileGlobs?.length > 0) {
502+
for (let i = 0; i < excludeFileGlobs.length; ++i) {
503+
if (matchesGlob(relativePath, excludeFileGlobs[i]) ||
504+
matchesGlob(absolutePath, excludeFileGlobs[i])) return true;
505+
}
506+
}
507+
508+
// This check filters out files that do not match the include globs.
509+
if (includeFileGlobs?.length > 0) {
510+
for (let i = 0; i < includeFileGlobs.length; ++i) {
511+
if (matchesGlob(relativePath, includeFileGlobs[i]) ||
512+
matchesGlob(absolutePath, includeFileGlobs[i])) return false;
513+
}
514+
return true;
515+
}
516+
517+
// This check filters out the node_modules/ directory, unless it is explicitly included.
518+
return StringPrototypeIncludes(url, '/node_modules/');
496519
}
497520

498-
function mergeCoverage(merged, coverage) {
521+
function mergeCoverage(merged, coverage, workingDirectory) {
499522
for (let i = 0; i < coverage.length; ++i) {
500523
const newScript = coverage[i];
501524
const { url } = newScript;
502525

503-
if (shouldSkipFileCoverage(url)) {
526+
if (shouldSkipFileCoverage(url, workingDirectory)) {
504527
continue;
505528
}
506529

src/node_options.cc

+8
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
686686
AddOption("--test-skip-pattern",
687687
"run tests whose name do not match this regular expression",
688688
&EnvironmentOptions::test_skip_pattern);
689+
AddOption("--test-coverage-include",
690+
"include files in coverage report that match this glob pattern",
691+
&EnvironmentOptions::coverage_include_pattern,
692+
kAllowedInEnvvar);
693+
AddOption("--test-coverage-exclude",
694+
"exclude files from coverage report that match this glob pattern",
695+
&EnvironmentOptions::coverage_exclude_pattern,
696+
kAllowedInEnvvar);
689697
AddOption("--test-udp-no-try-send", "", // For testing only.
690698
&EnvironmentOptions::test_udp_no_try_send);
691699
AddOption("--throw-deprecation",

src/node_options.h

+2
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ class EnvironmentOptions : public Options {
188188
bool test_udp_no_try_send = false;
189189
std::string test_shard;
190190
std::vector<std::string> test_skip_pattern;
191+
std::vector<std::string> coverage_include_pattern;
192+
std::vector<std::string> coverage_exclude_pattern;
191193
bool throw_deprecation = false;
192194
bool trace_atomics_wait = false;
193195
bool trace_deprecation = false;

test/parallel/test-runner-coverage.js

+93
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,96 @@ test('coverage with ESM hook - source transpiled', skipIfNoInspector, () => {
335335
assert(result.stdout.toString().includes(report));
336336
assert.strictEqual(result.status, 0);
337337
});
338+
339+
test('coverage with excluded files', skipIfNoInspector, () => {
340+
const fixture = fixtures.path('test-runner', 'coverage.js');
341+
const args = [
342+
'--experimental-test-coverage', '--test-reporter', 'tap',
343+
'--test-coverage-exclude=test/*/test-runner/invalid-tap.js',
344+
fixture];
345+
const result = spawnSync(process.execPath, args);
346+
const report = [
347+
'# start of coverage report',
348+
'# ' + '-'.repeat(112),
349+
'# file | line % | branch % | funcs % | uncovered lines',
350+
'# ' + '-'.repeat(112),
351+
'# test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72',
352+
'# test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5-6',
353+
'# ' + '-'.repeat(112),
354+
'# all files | 78.13 | 40.00 | 60.00 |',
355+
'# ' + '-'.repeat(112),
356+
'# end of coverage report',
357+
].join('\n');
358+
359+
360+
if (common.isWindows) {
361+
return report.replaceAll('/', '\\');
362+
}
363+
364+
assert(result.stdout.toString().includes(report));
365+
assert.strictEqual(result.status, 0);
366+
assert(!findCoverageFileForPid(result.pid));
367+
});
368+
369+
test('coverage with included files', skipIfNoInspector, () => {
370+
const fixture = fixtures.path('test-runner', 'coverage.js');
371+
const args = [
372+
'--experimental-test-coverage', '--test-reporter', 'tap',
373+
'--test-coverage-include=test/fixtures/test-runner/coverage.js',
374+
'--test-coverage-include=test/fixtures/v8-coverage/throw.js',
375+
fixture,
376+
];
377+
const result = spawnSync(process.execPath, args);
378+
const report = [
379+
'# start of coverage report',
380+
'# ' + '-'.repeat(112),
381+
'# file | line % | branch % | funcs % | uncovered lines',
382+
'# ' + '-'.repeat(112),
383+
'# test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72',
384+
'# test/fixtures/v8-coverage/throw.js | 71.43 | 50.00 | 100.00 | 5-6',
385+
'# ' + '-'.repeat(112),
386+
'# all files | 78.13 | 40.00 | 60.00 |',
387+
'# ' + '-'.repeat(112),
388+
'# end of coverage report',
389+
].join('\n');
390+
391+
392+
if (common.isWindows) {
393+
return report.replaceAll('/', '\\');
394+
}
395+
396+
assert(result.stdout.toString().includes(report));
397+
assert.strictEqual(result.status, 0);
398+
assert(!findCoverageFileForPid(result.pid));
399+
});
400+
401+
test('coverage with included and excluded files', skipIfNoInspector, () => {
402+
const fixture = fixtures.path('test-runner', 'coverage.js');
403+
const args = [
404+
'--experimental-test-coverage', '--test-reporter', 'tap',
405+
'--test-coverage-include=test/fixtures/test-runner/*.js',
406+
'--test-coverage-exclude=test/fixtures/test-runner/*-tap.js',
407+
fixture,
408+
];
409+
const result = spawnSync(process.execPath, args);
410+
const report = [
411+
'# start of coverage report',
412+
'# ' + '-'.repeat(112),
413+
'# file | line % | branch % | funcs % | uncovered lines',
414+
'# ' + '-'.repeat(112),
415+
'# test/fixtures/test-runner/coverage.js | 78.65 | 38.46 | 60.00 | 12-13 16-22 27 39 43-44 61-62 66-67 71-72',
416+
'# ' + '-'.repeat(112),
417+
'# all files | 78.65 | 38.46 | 60.00 |',
418+
'# ' + '-'.repeat(112),
419+
'# end of coverage report',
420+
].join('\n');
421+
422+
423+
if (common.isWindows) {
424+
return report.replaceAll('/', '\\');
425+
}
426+
427+
assert(result.stdout.toString().includes(report));
428+
assert.strictEqual(result.status, 0);
429+
assert(!findCoverageFileForPid(result.pid));
430+
});

0 commit comments

Comments
 (0)