Skip to content

Commit 9a9c295

Browse files
committed
test_runner: add support for coverage via run()
1 parent 29cf623 commit 9a9c295

File tree

2 files changed

+236
-1
lines changed

2 files changed

+236
-1
lines changed

lib/internal/test_runner/runner.js

+77-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const {
5454
validateObject,
5555
validateOneOf,
5656
validateInteger,
57+
validateStringArray,
5758
} = require('internal/validators');
5859
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
5960
const { isRegExp } = require('internal/util/types');
@@ -510,7 +511,16 @@ function watchFiles(testFiles, opts) {
510511
function run(options = kEmptyObject) {
511512
validateObject(options, 'options');
512513

513-
let { testNamePatterns, testSkipPatterns, shard } = options;
514+
let {
515+
testNamePatterns,
516+
testSkipPatterns,
517+
shard,
518+
coverageExcludeGlobs,
519+
coverageIncludeGlobs,
520+
lineCoverage,
521+
branchCoverage,
522+
functionCoverage,
523+
} = options;
514524
const {
515525
concurrency,
516526
timeout,
@@ -523,6 +533,7 @@ function run(options = kEmptyObject) {
523533
setup,
524534
only,
525535
globPatterns,
536+
coverage,
526537
} = options;
527538

528539
if (files != null) {
@@ -601,6 +612,65 @@ function run(options = kEmptyObject) {
601612
});
602613
}
603614
validateOneOf(isolation, 'options.isolation', ['process', 'none']);
615+
if (coverage != null) {
616+
validateBoolean(coverage, 'options.coverage');
617+
}
618+
if (coverageExcludeGlobs != null) {
619+
if (!coverage) {
620+
throw new ERR_INVALID_ARG_VALUE(
621+
'options.coverageExcludeGlobs',
622+
coverageExcludeGlobs,
623+
'is only supported when coverage is enabled',
624+
);
625+
}
626+
if (!ArrayIsArray(coverageExcludeGlobs)) {
627+
coverageExcludeGlobs = [coverageExcludeGlobs];
628+
}
629+
validateStringArray(coverageExcludeGlobs, 'options.coverageExcludeGlobs');
630+
}
631+
if (coverageIncludeGlobs != null) {
632+
if (!coverage) {
633+
throw new ERR_INVALID_ARG_VALUE(
634+
'options.coverageIncludeGlobs',
635+
coverageIncludeGlobs,
636+
'is only supported when coverage is enabled',
637+
);
638+
}
639+
if (!ArrayIsArray(coverageIncludeGlobs)) {
640+
coverageIncludeGlobs = [coverageIncludeGlobs];
641+
}
642+
validateStringArray(coverageIncludeGlobs, 'options.coverageIncludeGlobs');
643+
}
644+
if (lineCoverage != null) {
645+
if (!coverage) {
646+
throw new ERR_INVALID_ARG_VALUE(
647+
'options.lineCoverage',
648+
lineCoverage,
649+
'is only supported when coverage is enabled',
650+
);
651+
}
652+
validateInteger(lineCoverage, 'options.lineCoverage', 0, 100);
653+
}
654+
if (branchCoverage != null) {
655+
if (!coverage) {
656+
throw new ERR_INVALID_ARG_VALUE(
657+
'options.branchCoverage',
658+
branchCoverage,
659+
'is only supported when coverage is enabled',
660+
);
661+
}
662+
validateInteger(branchCoverage, 'options.branchCoverage', 0, 100);
663+
}
664+
if (functionCoverage != null) {
665+
if (!coverage) {
666+
throw new ERR_INVALID_ARG_VALUE(
667+
'options.functionCoverage',
668+
functionCoverage,
669+
'is only supported when coverage is enabled',
670+
);
671+
}
672+
validateInteger(functionCoverage, 'options.functionCoverage', 0, 100);
673+
}
604674

605675
const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
606676
const globalOptions = {
@@ -609,6 +679,12 @@ function run(options = kEmptyObject) {
609679
// behavior has relied on it, so removing it must be done in a semver major.
610680
...parseCommandLine(),
611681
setup, // This line can be removed when parseCommandLine() is removed here.
682+
coverage,
683+
coverageExcludeGlobs,
684+
coverageIncludeGlobs,
685+
lineCoverage,
686+
branchCoverage,
687+
functionCoverage,
612688
};
613689
const root = createTestTree(rootTestOptions, globalOptions);
614690
let testFiles = files ?? createTestFileList(globPatterns);
+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import * as common from '../common/index.mjs';
2+
import * as fixtures from '../common/fixtures.mjs';
3+
import { describe, it, run } from 'node:test';
4+
import assert from 'node:assert';
5+
6+
const files = [fixtures.path('test-runner', 'coverage.js')];
7+
const skipIfNoInspector = {
8+
skip: !process.features.inspector ? 'inspector disabled' : false
9+
};
10+
11+
describe('require(\'node:test\').run Coverage settings', { concurrency: true }, () => {
12+
describe('validation', () => {
13+
it('should only allow boolean in options.coverage', async () => {
14+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, '', '1', Promise.resolve(true), []]
15+
.forEach((coverage) => assert.throws(() => run({ coverage }), {
16+
code: 'ERR_INVALID_ARG_TYPE'
17+
}));
18+
});
19+
20+
it('should only allow coverage options when coverage is true', async () => {
21+
assert.throws(
22+
() => run({ coverage: false, coverageIncludeGlobs: [] }),
23+
{ code: 'ERR_INVALID_ARG_VALUE' },
24+
);
25+
assert.throws(
26+
() => run({ coverage: false, coverageExcludeGlobs: [] }),
27+
{ code: 'ERR_INVALID_ARG_VALUE' },
28+
);
29+
assert.throws(
30+
() => run({ coverage: false, lineCoverage: 0 }),
31+
{ code: 'ERR_INVALID_ARG_VALUE' },
32+
);
33+
assert.throws(
34+
() => run({ coverage: false, branchCoverage: 0 }),
35+
{ code: 'ERR_INVALID_ARG_VALUE' },
36+
);
37+
assert.throws(
38+
() => run({ coverage: false, functionCoverage: 0 }),
39+
{ code: 'ERR_INVALID_ARG_VALUE' },
40+
);
41+
});
42+
43+
it('should only allow string|string[] in options.coverageExcludeGlobs', async () => {
44+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
45+
.forEach((coverageExcludeGlobs) => {
46+
assert.throws(() => run({ coverage: true, coverageExcludeGlobs }), {
47+
code: 'ERR_INVALID_ARG_TYPE'
48+
});
49+
assert.throws(() => run({ coverage: true, coverageExcludeGlobs: [coverageExcludeGlobs] }), {
50+
code: 'ERR_INVALID_ARG_TYPE'
51+
});
52+
});
53+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludeGlobs: [''] });
54+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageExcludeGlobs: '' });
55+
});
56+
57+
it('should only allow string|string[] in options.coverageIncludeGlobs', async () => {
58+
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, Promise.resolve([]), true, false]
59+
.forEach((coverageIncludeGlobs) => {
60+
assert.throws(() => run({ coverage: true, coverageIncludeGlobs }), {
61+
code: 'ERR_INVALID_ARG_TYPE'
62+
});
63+
assert.throws(() => run({ coverage: true, coverageIncludeGlobs: [coverageIncludeGlobs] }), {
64+
code: 'ERR_INVALID_ARG_TYPE'
65+
});
66+
});
67+
68+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludeGlobs: [''] });
69+
run({ files: [], signal: AbortSignal.abort(), coverage: true, coverageIncludeGlobs: '' });
70+
});
71+
72+
it('should only allow an int in options.lineCoverage', async () => {
73+
[Symbol(), {}, () => {}, [], 0n, 1n, Promise.resolve([]), true, false]
74+
.forEach((lineCoverage) => {
75+
assert.throws(() => run({ coverage: true, lineCoverage }), {
76+
code: 'ERR_INVALID_ARG_TYPE'
77+
});
78+
assert.throws(() => run({ coverage: true, lineCoverage: [lineCoverage] }), {
79+
code: 'ERR_INVALID_ARG_TYPE'
80+
});
81+
});
82+
83+
run({ files: [], signal: AbortSignal.abort(), coverage: true, lineCoverage: 0 });
84+
});
85+
86+
it('should only allow an int in options.branchCoverage', async () => {
87+
[Symbol(), {}, () => {}, [], 0n, 1n, Promise.resolve([]), true, false]
88+
.forEach((branchCoverage) => {
89+
assert.throws(() => run({ coverage: true, branchCoverage }), {
90+
code: 'ERR_INVALID_ARG_TYPE'
91+
});
92+
assert.throws(() => run({ coverage: true, branchCoverage: [branchCoverage] }), {
93+
code: 'ERR_INVALID_ARG_TYPE'
94+
});
95+
});
96+
97+
run({ files: [], signal: AbortSignal.abort(), coverage: true, branchCoverage: 0 });
98+
});
99+
100+
it('should only allow an int in options.functionCoverage', async () => {
101+
[Symbol(), {}, () => {}, [], 0n, 1n, Promise.resolve([]), true, false]
102+
.forEach((functionCoverage) => {
103+
assert.throws(() => run({ coverage: true, functionCoverage }), {
104+
code: 'ERR_INVALID_ARG_TYPE'
105+
});
106+
assert.throws(() => run({ coverage: true, functionCoverage: [functionCoverage] }), {
107+
code: 'ERR_INVALID_ARG_TYPE'
108+
});
109+
});
110+
111+
run({ files: [], signal: AbortSignal.abort(), coverage: true, functionCoverage: 0 });
112+
});
113+
});
114+
115+
describe('run with coverage', skipIfNoInspector, () => {
116+
it('should run with coverage', async () => {
117+
const stream = run({ files, coverage: true });
118+
stream.on('test:fail', common.mustNotCall());
119+
stream.on('test:pass', common.mustCall());
120+
stream.on('test:coverage', common.mustCall());
121+
// eslint-disable-next-line no-unused-vars
122+
for await (const _ of stream);
123+
});
124+
125+
it('should run with coverage and exclude by glob', async () => {
126+
const stream = run({ files, coverage: true, coverageExcludePatterns: ['test/*/test-runner/invalid-tap.js'] });
127+
stream.on('test:fail', common.mustNotCall());
128+
stream.on('test:pass', common.mustCall(1));
129+
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
130+
const filesPaths = files.map(({ path }) => path);
131+
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), false);
132+
}));
133+
// eslint-disable-next-line no-unused-vars
134+
for await (const _ of stream);
135+
});
136+
137+
it('should run with coverage and include by glob', async () => {
138+
const stream = run({ files, coverage: true, coverageIncludePatterns: ['test/*/test-runner/invalid-tap.js'] });
139+
stream.on('test:fail', common.mustNotCall());
140+
stream.on('test:pass', common.mustCall(1));
141+
stream.on('test:coverage', common.mustCall(({ summary: { files } }) => {
142+
const filesPaths = files.map(({ path }) => path);
143+
assert.strictEqual(filesPaths.some((path) => path.includes('test-runner/invalid-tap.js')), true);
144+
}));
145+
// eslint-disable-next-line no-unused-vars
146+
for await (const _ of stream);
147+
});
148+
});
149+
});
150+
151+
152+
// exitHandler doesn't run until after the tests / after hooks finish.
153+
process.on('exit', () => {
154+
assert.strictEqual(process.listeners('uncaughtException').length, 0);
155+
assert.strictEqual(process.listeners('unhandledRejection').length, 0);
156+
assert.strictEqual(process.listeners('beforeExit').length, 0);
157+
assert.strictEqual(process.listeners('SIGINT').length, 0);
158+
assert.strictEqual(process.listeners('SIGTERM').length, 0);
159+
});

0 commit comments

Comments
 (0)