Skip to content

Commit 2907841

Browse files
committed
test_runner: add support for coverage via run()
1 parent 16fca17 commit 2907841

File tree

5 files changed

+183
-9
lines changed

5 files changed

+183
-9
lines changed

doc/api/test.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,9 @@ added:
12391239
- v18.9.0
12401240
- v16.19.0
12411241
changes:
1242+
- version: REPLACEME
1243+
pr-url: https://github.com/nodejs/node/pull/53937
1244+
description: Added coverage options.
12421245
- version: REPLACEME
12431246
pr-url: https://github.com/nodejs/node/pull/53866
12441247
description: Added the `globPatterns` option.
@@ -1304,6 +1307,14 @@ changes:
13041307
that specifies the index of the shard to run. This option is _required_.
13051308
* `total` {number} is a positive integer that specifies the total number
13061309
of shards to split the test files to. This option is _required_.
1310+
* `coverage` {boolean} Whether to collect code coverage or not.
1311+
**Default:** `false`.
1312+
* `coverageIncludePatterns` {string|Array} Includes specific files in code coverage using a
1313+
glob expression, which can match both absolute and relative file paths.
1314+
**Default:** `undefined`.
1315+
* `coverageExcludePatterns` {string|Array} Excludes specific files from code coverage using
1316+
a glob expression, which can match both absolute and relative file paths.
1317+
**Default:** `undefined`.
13071318
* Returns: {TestsStream}
13081319

13091320
**Note:** `shard` is used to horizontally parallelize test running across

lib/internal/test_runner/coverage.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,8 +495,8 @@ function setupCoverage(options) {
495495
coverageDirectory,
496496
originalCoverageDirectory,
497497
cwd,
498-
options.coverageExcludeGlobs,
499-
options.coverageIncludeGlobs,
498+
options.coverageExcludePatterns,
499+
options.coverageIncludePatterns,
500500
);
501501
}
502502

lib/internal/test_runner/runner.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const {
5050
validateFunction,
5151
validateObject,
5252
validateInteger,
53+
validateStringArray,
5354
} = require('internal/validators');
5455
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
5556
const { isRegExp } = require('internal/util/types');
@@ -471,7 +472,13 @@ function watchFiles(testFiles, opts) {
471472
function run(options = kEmptyObject) {
472473
validateObject(options, 'options');
473474

474-
let { testNamePatterns, testSkipPatterns, shard } = options;
475+
let {
476+
testNamePatterns,
477+
testSkipPatterns,
478+
shard,
479+
coverageExcludePatterns,
480+
coverageIncludePatterns,
481+
} = options;
475482
const {
476483
concurrency,
477484
timeout,
@@ -483,6 +490,7 @@ function run(options = kEmptyObject) {
483490
setup,
484491
only,
485492
globPatterns,
493+
coverage,
486494
} = options;
487495

488496
if (files != null) {
@@ -560,10 +568,43 @@ function run(options = kEmptyObject) {
560568
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value);
561569
});
562570
}
571+
if (coverage != null) {
572+
validateBoolean(coverage, 'options.coverage');
573+
}
574+
if (coverageExcludePatterns != null) {
575+
if (!coverage) {
576+
throw new ERR_INVALID_ARG_VALUE(
577+
'options.coverageExcludePatterns',
578+
coverageExcludePatterns,
579+
'is only supported when coverage is enabled',
580+
);
581+
}
582+
if (!ArrayIsArray(coverageExcludePatterns)) {
583+
coverageExcludePatterns = [coverageExcludePatterns];
584+
}
585+
validateStringArray(coverageExcludePatterns, 'options.coverageExcludePatterns');
586+
}
587+
if (coverageIncludePatterns != null) {
588+
if (!coverage) {
589+
throw new ERR_INVALID_ARG_VALUE(
590+
'options.coverageIncludePatterns',
591+
coverageIncludePatterns,
592+
'is only supported when coverage is enabled',
593+
);
594+
}
595+
if (!ArrayIsArray(coverageIncludePatterns)) {
596+
coverageIncludePatterns = [coverageIncludePatterns];
597+
}
598+
validateStringArray(coverageIncludePatterns, 'options.coverageIncludePatterns');
599+
}
600+
563601
const root = createTestTree(
564602
{ __proto__: null, concurrency, timeout, signal },
565603
{
566604
__proto__: null,
605+
coverage,
606+
coverageExcludePatterns,
607+
coverageIncludePatterns,
567608
forceExit,
568609
perFileTimeout: timeout || Infinity,
569610
runnerConcurrency: concurrency,

lib/internal/test_runner/utils.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ function parseCommandLine() {
200200
const watchMode = getOptionValue('--watch');
201201
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
202202
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
203-
let coverageExcludeGlobs;
204-
let coverageIncludeGlobs;
203+
let coverageExcludePatterns;
204+
let coverageIncludePatterns;
205205
let destinations;
206206
let perFileTimeout;
207207
let reporters;
@@ -277,16 +277,16 @@ function parseCommandLine() {
277277
}
278278

279279
if (coverage) {
280-
coverageExcludeGlobs = getOptionValue('--test-coverage-exclude');
281-
coverageIncludeGlobs = getOptionValue('--test-coverage-include');
280+
coverageExcludePatterns = getOptionValue('--test-coverage-exclude');
281+
coverageIncludePatterns = getOptionValue('--test-coverage-include');
282282
}
283283

284284
globalTestOptions = {
285285
__proto__: null,
286286
isTestRunner,
287287
coverage,
288-
coverageExcludeGlobs,
289-
coverageIncludeGlobs,
288+
coverageExcludePatterns,
289+
coverageIncludePatterns,
290290
forceExit,
291291
perFileTimeout,
292292
runnerConcurrency,

test/parallel/test-runner-run.mjs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { dot, spec, tap } from 'node:test/reporters';
66
import assert from 'node:assert';
77

88
const testFixtures = fixtures.path('test-runner');
9+
const skipIfNoInspector = {
10+
skip: !process.features.inspector ? 'inspector disabled' : false
11+
};
912

1013
describe('require(\'node:test\').run', { concurrency: true }, () => {
1114
it('should run with no tests', async () => {
@@ -502,6 +505,125 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
502505
});
503506
});
504507

508+
describe('coverage', () => {
509+
describe('validation', () => {
510+
511+
it('should only allow boolean in options.coverage', async () => {
512+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, '', '1', Promise.resolve(true), []]
513+
.forEach((coverage) => assert.throws(() => run({ coverage }), {
514+
code: 'ERR_INVALID_ARG_TYPE'
515+
}));
516+
});
517+
518+
it('should only allow coverageExcludePatterns and coverageIncludePatterns when coverage is true', async () => {
519+
assert.throws(
520+
() => run({ coverage: false, coverageIncludePatterns: [] }),
521+
{ code: 'ERR_INVALID_ARG_VALUE' },
522+
);
523+
assert.throws(
524+
() => run({ coverage: false, coverageExcludePatterns: [] }),
525+
{ code: 'ERR_INVALID_ARG_VALUE' },
526+
);
527+
});
528+
529+
it('should only allow string|string[] in options.coverageExcludePatterns', async () => {
530+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
531+
.forEach((coverageExcludePatterns) => {
532+
assert.throws(() => run({ coverage: true, coverageExcludePatterns }), {
533+
code: 'ERR_INVALID_ARG_TYPE'
534+
});
535+
assert.throws(() => run({ coverage: true, coverageExcludePatterns: [coverageExcludePatterns] }), {
536+
code: 'ERR_INVALID_ARG_TYPE'
537+
});
538+
});
539+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludePatterns: [''] });
540+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludePatterns: '' });
541+
});
542+
543+
it('should only allow string|string[] in options.coverageIncludePatterns', async () => {
544+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
545+
.forEach((coverageIncludePatterns) => {
546+
assert.throws(() => run({ coverage: true, coverageIncludePatterns }), {
547+
code: 'ERR_INVALID_ARG_TYPE'
548+
});
549+
assert.throws(() => run({ coverage: true, coverageIncludePatterns: [coverageIncludePatterns] }), {
550+
code: 'ERR_INVALID_ARG_TYPE'
551+
});
552+
});
553+
554+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludePatterns: [''] });
555+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludePatterns: '' });
556+
});
557+
});
558+
559+
const files = [fixtures.path('test-runner', 'coverage.js')];
560+
it('should run with coverage', skipIfNoInspector, async () => {
561+
const stream = run({ files, coverage: true });
562+
stream.on('test:fail', common.mustNotCall());
563+
stream.on('test:pass', common.mustCall(1));
564+
stream.on('test:coverage', common.mustCall());
565+
// eslint-disable-next-line no-unused-vars
566+
for await (const _ of stream);
567+
});
568+
569+
it('should run with coverage and exclude by glob', skipIfNoInspector, async () => {
570+
const stream = run({ files, coverage: true, coverageExcludePatterns: ['test/*/test-runner/invalid-tap.js'] });
571+
stream.on('test:fail', common.mustNotCall());
572+
stream.on('test:pass', common.mustCall(1));
573+
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
574+
const filesPaths = files.map(({ path }) => path);
575+
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), false);
576+
}));
577+
// eslint-disable-next-line no-unused-vars
578+
for await (const _ of stream);
579+
});
580+
581+
it('should run with coverage and include by glob', skipIfNoInspector, async () => {
582+
const stream = run({ files, coverage: true, coverageIncludePatterns: ['test/*/test-runner/invalid-tap.js'] });
583+
stream.on('test:fail', common.mustNotCall());
584+
stream.on('test:pass', common.mustCall(1));
585+
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
586+
const filesPaths = files.map(({ path }) => path);
587+
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), true);
588+
}));
589+
// eslint-disable-next-line no-unused-vars
590+
for await (const _ of stream);
591+
});
592+
});
593+
594+
it('should run with no files', async () => {
595+
const stream = run({
596+
files: undefined
597+
}).compose(tap);
598+
stream.on('test:fail', common.mustNotCall());
599+
stream.on('test:pass', common.mustNotCall());
600+
601+
// eslint-disable-next-line no-unused-vars
602+
for await (const _ of stream);
603+
});
604+
605+
it('should run with no files and use spec reporter', async () => {
606+
const stream = run({
607+
files: undefined
608+
}).compose(spec);
609+
stream.on('test:fail', common.mustNotCall());
610+
stream.on('test:pass', common.mustNotCall());
611+
612+
// eslint-disable-next-line no-unused-vars
613+
for await (const _ of stream);
614+
});
615+
616+
it('should run with no files and use dot reporter', async () => {
617+
const stream = run({
618+
files: undefined
619+
}).compose(dot);
620+
stream.on('test:fail', common.mustNotCall());
621+
stream.on('test:pass', common.mustNotCall());
622+
623+
// eslint-disable-next-line no-unused-vars
624+
for await (const _ of stream);
625+
});
626+
505627
it('should avoid running recursively', async () => {
506628
const stream = run({ files: [join(testFixtures, 'recursive_run.js')] });
507629
let stderr = '';

0 commit comments

Comments
 (0)