Skip to content

Commit bc609d0

Browse files
committed
test: support glob matching coverage files
1 parent 92a25ab commit bc609d0

File tree

6 files changed

+173
-7
lines changed

6 files changed

+173
-7
lines changed

doc/api/cli.md

Lines changed: 36 additions & 0 deletions
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 in 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
@@ -2908,6 +2942,8 @@ one is included in the list below.
29082942
* `--secure-heap-min`
29092943
* `--secure-heap`
29102944
* `--snapshot-blob`
2945+
* `--test-coverage-exclude`
2946+
* `--test-coverage-include`
29112947
* `--test-only`
29122948
* `--test-reporter-destination`
29132949
* `--test-reporter`

doc/node.1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,12 @@ Starts the Node.js command line test runner.
435435
The maximum number of test files that the test runner CLI will execute
436436
concurrently.
437437
.
438+
.It Fl -test-coverage-exclude
439+
A glob pattern that excludes matching files from the coverage report
440+
.
441+
.It Fl -test-coverage-include
442+
A glob pattern that only includes matching files in the coverage report
443+
.
438444
.It Fl -test-force-exit
439445
Configures the test runner to exit the process once all known tests have
440446
finished executing even if the event loop would otherwise remain active.

lib/internal/test_runner/coverage.js

Lines changed: 27 additions & 7 deletions
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,39 @@ function mapRangeToLines(range, lines) {
485488
return { __proto__: null, lines: mappedLines, ignoredLines };
486489
}
487490

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

498-
function mergeCoverage(merged, coverage) {
518+
function mergeCoverage(merged, coverage, workingDirectory) {
499519
for (let i = 0; i < coverage.length; ++i) {
500520
const newScript = coverage[i];
501521
const { url } = newScript;
502522

503-
if (shouldSkipFileCoverage(url)) {
523+
if (shouldSkipFileCoverage(url, workingDirectory)) {
504524
continue;
505525
}
506526

src/node_options.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,14 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
670670
AddOption("--test-skip-pattern",
671671
"run tests whose name do not match this regular expression",
672672
&EnvironmentOptions::test_skip_pattern);
673+
AddOption("--test-coverage-include",
674+
"include files in coverage report that match this glob pattern",
675+
&EnvironmentOptions::coverage_include_pattern,
676+
kAllowedInEnvvar);
677+
AddOption("--test-coverage-exclude",
678+
"exclude files in coverage report that match this glob pattern",
679+
&EnvironmentOptions::coverage_exclude_pattern,
680+
kAllowedInEnvvar);
673681
AddOption("--test-udp-no-try-send", "", // For testing only.
674682
&EnvironmentOptions::test_udp_no_try_send);
675683
AddOption("--throw-deprecation",

src/node_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ class EnvironmentOptions : public Options {
186186
bool test_udp_no_try_send = false;
187187
std::string test_shard;
188188
std::vector<std::string> test_skip_pattern;
189+
std::vector<std::string> coverage_include_pattern;
190+
std::vector<std::string> coverage_exclude_pattern;
189191
bool throw_deprecation = false;
190192
bool trace_deprecation = false;
191193
bool trace_exit = false;

test/parallel/test-runner-coverage.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,97 @@ 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+
console.log(result.stdout.toString());
428+
assert(result.stdout.toString().includes(report));
429+
assert.strictEqual(result.status, 0);
430+
assert(!findCoverageFileForPid(result.pid));
431+
});

0 commit comments

Comments
 (0)