Skip to content

Commit bb211c1

Browse files
committed
test_runner: added coverage threshold support for tests
1 parent c1051a0 commit bb211c1

7 files changed

Lines changed: 189 additions & 54 deletions

File tree

lib/internal/test_runner/coverage.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
3131
const kLineEndingRegex = /\r?\n$/u;
3232
const kLineSplitRegex = /(?<=\r?\n)/u;
3333
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
34+
const kDefaultMinimumCoverage = {
35+
__proto__: null,
36+
line: 0,
37+
function: 0,
38+
branch: 0,
39+
};
3440

3541
class CoverageLine {
3642
#covered;
@@ -63,10 +69,11 @@ class CoverageLine {
6369
}
6470

6571
class TestCoverage {
66-
constructor(coverageDirectory, originalCoverageDirectory, workingDirectory) {
72+
constructor(coverageDirectory, originalCoverageDirectory, workingDirectory, minimumCoverage) {
6773
this.coverageDirectory = coverageDirectory;
6874
this.originalCoverageDirectory = originalCoverageDirectory;
6975
this.workingDirectory = workingDirectory;
76+
this.minimumCoverage = minimumCoverage || kDefaultMinimumCoverage;
7077
}
7178

7279
summary() {
@@ -260,6 +267,24 @@ class TestCoverage {
260267
);
261268
coverageSummary.files.sort(sortCoverageFiles);
262269

270+
coverageSummary.minimumCoverage = {
271+
__proto__: null,
272+
line: {
273+
status: doesCoveragePass(this.minimumCoverage.line, coverageSummary.totals.coveredLinePercent),
274+
expected: this.minimumCoverage.line,
275+
actual: coverageSummary.totals.coveredLinePercent,
276+
},
277+
function: {
278+
status: doesCoveragePass(this.minimumCoverage.function, coverageSummary.totals.coveredFunctionPercent),
279+
expected:this.minimumCoverage.function,
280+
actual: coverageSummary.totals.coveredFunctionPercent
281+
},
282+
branch: {
283+
status: doesCoveragePass(this.minimumCoverage.branch, coverageSummary.totals.coveredBranchPercent),
284+
expected: this.minimumCoverage.branch,
285+
actual: coverageSummary.totals.coveredBranchPercent
286+
},
287+
};
263288
return coverageSummary;
264289
}
265290

@@ -299,7 +324,7 @@ function sortCoverageFiles(a, b) {
299324
return StringPrototypeLocaleCompare(a.path, b.path);
300325
}
301326

302-
function setupCoverage() {
327+
function setupCoverage(minimumCoverage = null) {
303328
let originalCoverageDirectory = process.env.NODE_V8_COVERAGE;
304329
const cwd = process.cwd();
305330

@@ -323,7 +348,7 @@ function setupCoverage() {
323348
// child processes.
324349
process.env.NODE_V8_COVERAGE = coverageDirectory;
325350

326-
return new TestCoverage(coverageDirectory, originalCoverageDirectory, cwd);
351+
return new TestCoverage(coverageDirectory, originalCoverageDirectory, cwd, minimumCoverage);
327352
}
328353

329354
function mapRangeToLines(range, lines) {
@@ -538,4 +563,8 @@ function doesRangeContainOtherRange(range, otherRange) {
538563
range.endOffset >= otherRange.endOffset;
539564
}
540565

566+
function doesCoveragePass(compareValue, actualValue) {
567+
return compareValue ? actualValue >= compareValue : true;
568+
}
569+
541570
module.exports = { setupCoverage, TestCoverage };

lib/internal/test_runner/harness.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
'use strict';
22
const {
3+
ArrayPrototypeFilter,
34
ArrayPrototypeForEach,
45
FunctionPrototypeBind,
56
PromiseResolve,
67
SafeMap,
8+
StringPrototypeSplit,
9+
StringPrototypeStartsWith,
710
} = primordials;
811
const { getCallerLocation } = internalBinding('util');
912
const {
@@ -78,14 +81,22 @@ function createProcessEventHandler(eventName, rootTest) {
7881
}
7982

8083
function configureCoverage(rootTest, globalOptions) {
81-
if (!globalOptions.coverage) {
82-
return null;
83-
}
84+
let minimumCoverage = rootTest.coverage;
85+
if (!minimumCoverage) {
86+
if (!globalOptions.coverage) {
87+
return null;
88+
}
8489

90+
const { 1: value } = StringPrototypeSplit(
91+
ArrayPrototypeFilter(process.execArgv, (arg) =>
92+
StringPrototypeStartsWith(arg, '--experimental-test-coverage')), '=');
93+
94+
minimumCoverage = { __proto__: null, line: value, function: value, branch: value };
95+
}
8596
const { setupCoverage } = require('internal/test_runner/coverage');
8697

8798
try {
88-
return setupCoverage();
99+
return setupCoverage(minimumCoverage);
89100
} catch (err) {
90101
const msg = `Warning: Code coverage could not be enabled. ${err}`;
91102

lib/internal/test_runner/runner.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ function run(options = kEmptyObject) {
439439
validateObject(options, 'options');
440440

441441
let { testNamePatterns, shard } = options;
442-
const { concurrency, timeout, signal, files, inspectPort, watch, setup, only } = options;
442+
const { concurrency, timeout, signal, files, inspectPort, watch, setup, only, coverage } = options;
443443

444444
if (files != null) {
445445
validateArray(files, 'options.files');
@@ -486,7 +486,15 @@ function run(options = kEmptyObject) {
486486
});
487487
}
488488

489-
const root = createTestTree({ __proto__: null, concurrency, timeout, signal });
489+
if (coverage != null) {
490+
if (typeof coverage === 'boolean') {
491+
validateBoolean(coverage, 'options.coverage');
492+
} else {
493+
validateObject(coverage, 'options.coverage');
494+
}
495+
}
496+
497+
const root = createTestTree({ __proto__: null, concurrency, timeout, signal, coverage });
490498
if (process.env.NODE_TEST_CONTEXT !== undefined) {
491499
return root.reporter;
492500
}

lib/internal/test_runner/test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
const {
3+
ArrayPrototypeFilter,
34
ArrayPrototypePush,
45
ArrayPrototypePushApply,
56
ArrayPrototypeReduce,
@@ -10,6 +11,8 @@ const {
1011
FunctionPrototype,
1112
MathMax,
1213
Number,
14+
ObjectDefineProperty,
15+
ObjectKeys,
1316
ObjectSeal,
1417
PromisePrototypeThen,
1518
PromiseResolve,
@@ -21,7 +24,6 @@ const {
2124
SafePromiseAll,
2225
SafePromiseRace,
2326
SymbolDispose,
24-
ObjectDefineProperty,
2527
Symbol,
2628
} = primordials;
2729
const { getCallerLocation } = internalBinding('util');
@@ -209,7 +211,7 @@ class Test extends AsyncResource {
209211
super('Test');
210212

211213
let { fn, name, parent, skip } = options;
212-
const { concurrency, loc, only, timeout, todo, signal } = options;
214+
const { concurrency, loc, only, timeout, todo, signal, coverage } = options;
213215

214216
if (typeof fn !== 'function') {
215217
fn = noop;
@@ -239,6 +241,7 @@ class Test extends AsyncResource {
239241
beforeEach: [],
240242
afterEach: [],
241243
};
244+
this.coverage = coverage;
242245
} else {
243246
const nesting = parent.parent === null ? parent.nesting :
244247
parent.nesting + 1;
@@ -258,6 +261,7 @@ class Test extends AsyncResource {
258261
beforeEach: ArrayPrototypeSlice(parent.hooks.beforeEach),
259262
afterEach: ArrayPrototypeSlice(parent.hooks.afterEach),
260263
};
264+
this.coverage = parent.coverage;
261265
}
262266

263267
switch (typeof concurrency) {
@@ -754,6 +758,11 @@ class Test extends AsyncResource {
754758

755759
if (coverage) {
756760
reporter.coverage(nesting, loc, coverage);
761+
const failedCoverage = ArrayPrototypeFilter(
762+
ObjectKeys(coverage.minimumCoverage), (key) => !coverage.minimumCoverage[key].status);
763+
if (failedCoverage.length > 0) {
764+
process.exitCode = 1;
765+
}
757766
}
758767

759768
reporter.end();

lib/internal/test_runner/utils.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ const {
33
ArrayPrototypeJoin,
44
ArrayPrototypeMap,
55
ArrayPrototypeFlatMap,
6+
ArrayPrototypeForEach,
67
ArrayPrototypePush,
78
ArrayPrototypeReduce,
89
ObjectGetOwnPropertyDescriptor,
10+
ObjectKeys,
911
MathFloor,
1012
MathMax,
1113
MathMin,
1214
NumberPrototypeToFixed,
13-
SafePromiseAllReturnArrayLike,
1415
RegExp,
1516
RegExpPrototypeExec,
1617
SafeMap,
18+
SafePromiseAllReturnArrayLike,
1719
StringPrototypePadStart,
1820
StringPrototypePadEnd,
1921
StringPrototypeRepeat,
@@ -316,6 +318,7 @@ const kSeparator = ' | ';
316318

317319
function getCoverageReport(pad, summary, symbol, color, table) {
318320
const prefix = `${pad}${symbol}`;
321+
color ||= white;
319322
let report = `${color}${prefix}start of coverage report\n`;
320323

321324
let filePadLength;
@@ -405,6 +408,13 @@ function getCoverageReport(pad, summary, symbol, color, table) {
405408
`${ArrayPrototypeJoin(ArrayPrototypeMap(kColumnsKeys, (columnKey, j) => getCell(NumberPrototypeToFixed(summary.totals[columnKey], 2), columnPadLengths[j], StringPrototypePadStart, false, summary.totals[columnKey])), kSeparator)} |\n`;
406409
if (table) report += addTableLine(prefix, tableWidth);
407410

411+
ArrayPrototypeForEach(ObjectKeys(summary.minimumCoverage), (key) => {
412+
const { actual, expected, status } = summary.minimumCoverage[key]
413+
if (!status) {
414+
report += `${red}${pad}${prefix}minimum coverage failed for ${key}. expected: ${expected} | actual: ${actual}\n${color}`;
415+
}
416+
});
417+
408418
report += `${prefix}end of coverage report\n`;
409419
if (color) {
410420
report += white;

0 commit comments

Comments
 (0)