Skip to content

Commit

Permalink
test_runner: added coverage threshold support for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pulkit-30 committed Dec 20, 2023
1 parent c1051a0 commit bb211c1
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 54 deletions.
35 changes: 32 additions & 3 deletions lib/internal/test_runner/coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const kIgnoreRegex = /\/\* node:coverage ignore next (?<count>\d+ )?\*\//;
const kLineEndingRegex = /\r?\n$/u;
const kLineSplitRegex = /(?<=\r?\n)/u;
const kStatusRegex = /\/\* node:coverage (?<status>enable|disable) \*\//;
const kDefaultMinimumCoverage = {
__proto__: null,
line: 0,
function: 0,
branch: 0,
};

class CoverageLine {
#covered;
Expand Down Expand Up @@ -63,10 +69,11 @@ class CoverageLine {
}

class TestCoverage {
constructor(coverageDirectory, originalCoverageDirectory, workingDirectory) {
constructor(coverageDirectory, originalCoverageDirectory, workingDirectory, minimumCoverage) {
this.coverageDirectory = coverageDirectory;
this.originalCoverageDirectory = originalCoverageDirectory;
this.workingDirectory = workingDirectory;
this.minimumCoverage = minimumCoverage || kDefaultMinimumCoverage;
}

summary() {
Expand Down Expand Up @@ -260,6 +267,24 @@ class TestCoverage {
);
coverageSummary.files.sort(sortCoverageFiles);

coverageSummary.minimumCoverage = {
__proto__: null,
line: {

Check failure on line 272 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Every object must have __proto__: null
status: doesCoveragePass(this.minimumCoverage.line, coverageSummary.totals.coveredLinePercent),
expected: this.minimumCoverage.line,
actual: coverageSummary.totals.coveredLinePercent,
},
function: {

Check failure on line 277 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Every object must have __proto__: null
status: doesCoveragePass(this.minimumCoverage.function, coverageSummary.totals.coveredFunctionPercent),
expected:this.minimumCoverage.function,

Check failure on line 279 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing space before value for key 'expected'
actual: coverageSummary.totals.coveredFunctionPercent

Check failure on line 280 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
},
branch: {

Check failure on line 282 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Every object must have __proto__: null
status: doesCoveragePass(this.minimumCoverage.branch, coverageSummary.totals.coveredBranchPercent),
expected: this.minimumCoverage.branch,
actual: coverageSummary.totals.coveredBranchPercent

Check failure on line 285 in lib/internal/test_runner/coverage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing trailing comma
},
};
return coverageSummary;
}

Expand Down Expand Up @@ -299,7 +324,7 @@ function sortCoverageFiles(a, b) {
return StringPrototypeLocaleCompare(a.path, b.path);
}

function setupCoverage() {
function setupCoverage(minimumCoverage = null) {
let originalCoverageDirectory = process.env.NODE_V8_COVERAGE;
const cwd = process.cwd();

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

return new TestCoverage(coverageDirectory, originalCoverageDirectory, cwd);
return new TestCoverage(coverageDirectory, originalCoverageDirectory, cwd, minimumCoverage);
}

function mapRangeToLines(range, lines) {
Expand Down Expand Up @@ -538,4 +563,8 @@ function doesRangeContainOtherRange(range, otherRange) {
range.endOffset >= otherRange.endOffset;
}

function doesCoveragePass(compareValue, actualValue) {
return compareValue ? actualValue >= compareValue : true;
}

module.exports = { setupCoverage, TestCoverage };
19 changes: 15 additions & 4 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeForEach,
FunctionPrototypeBind,
PromiseResolve,
SafeMap,
StringPrototypeSplit,
StringPrototypeStartsWith,
} = primordials;
const { getCallerLocation } = internalBinding('util');
const {
Expand Down Expand Up @@ -78,14 +81,22 @@ function createProcessEventHandler(eventName, rootTest) {
}

function configureCoverage(rootTest, globalOptions) {
if (!globalOptions.coverage) {
return null;
}
let minimumCoverage = rootTest.coverage;
if (!minimumCoverage) {
if (!globalOptions.coverage) {
return null;
}

const { 1: value } = StringPrototypeSplit(
ArrayPrototypeFilter(process.execArgv, (arg) =>
StringPrototypeStartsWith(arg, '--experimental-test-coverage')), '=');

minimumCoverage = { __proto__: null, line: value, function: value, branch: value };
}
const { setupCoverage } = require('internal/test_runner/coverage');

try {
return setupCoverage();
return setupCoverage(minimumCoverage);
} catch (err) {
const msg = `Warning: Code coverage could not be enabled. ${err}`;

Expand Down
12 changes: 10 additions & 2 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ function run(options = kEmptyObject) {
validateObject(options, 'options');

let { testNamePatterns, shard } = options;
const { concurrency, timeout, signal, files, inspectPort, watch, setup, only } = options;
const { concurrency, timeout, signal, files, inspectPort, watch, setup, only, coverage } = options;

if (files != null) {
validateArray(files, 'options.files');
Expand Down Expand Up @@ -486,7 +486,15 @@ function run(options = kEmptyObject) {
});
}

const root = createTestTree({ __proto__: null, concurrency, timeout, signal });
if (coverage != null) {
if (typeof coverage === 'boolean') {
validateBoolean(coverage, 'options.coverage');
} else {
validateObject(coverage, 'options.coverage');
}
}

const root = createTestTree({ __proto__: null, concurrency, timeout, signal, coverage });
if (process.env.NODE_TEST_CONTEXT !== undefined) {
return root.reporter;
}
Expand Down
13 changes: 11 additions & 2 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeReduce,
Expand All @@ -10,6 +11,8 @@ const {
FunctionPrototype,
MathMax,
Number,
ObjectDefineProperty,
ObjectKeys,
ObjectSeal,
PromisePrototypeThen,
PromiseResolve,
Expand All @@ -21,7 +24,6 @@ const {
SafePromiseAll,
SafePromiseRace,
SymbolDispose,
ObjectDefineProperty,
Symbol,
} = primordials;
const { getCallerLocation } = internalBinding('util');
Expand Down Expand Up @@ -209,7 +211,7 @@ class Test extends AsyncResource {
super('Test');

let { fn, name, parent, skip } = options;
const { concurrency, loc, only, timeout, todo, signal } = options;
const { concurrency, loc, only, timeout, todo, signal, coverage } = options;

if (typeof fn !== 'function') {
fn = noop;
Expand Down Expand Up @@ -239,6 +241,7 @@ class Test extends AsyncResource {
beforeEach: [],
afterEach: [],
};
this.coverage = coverage;
} else {
const nesting = parent.parent === null ? parent.nesting :
parent.nesting + 1;
Expand All @@ -258,6 +261,7 @@ class Test extends AsyncResource {
beforeEach: ArrayPrototypeSlice(parent.hooks.beforeEach),
afterEach: ArrayPrototypeSlice(parent.hooks.afterEach),
};
this.coverage = parent.coverage;
}

switch (typeof concurrency) {
Expand Down Expand Up @@ -754,6 +758,11 @@ class Test extends AsyncResource {

if (coverage) {
reporter.coverage(nesting, loc, coverage);
const failedCoverage = ArrayPrototypeFilter(
ObjectKeys(coverage.minimumCoverage), (key) => !coverage.minimumCoverage[key].status);
if (failedCoverage.length > 0) {
process.exitCode = 1;
}
}

reporter.end();
Expand Down
12 changes: 11 additions & 1 deletion lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ const {
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypeFlatMap,
ArrayPrototypeForEach,
ArrayPrototypePush,
ArrayPrototypeReduce,
ObjectGetOwnPropertyDescriptor,
ObjectKeys,
MathFloor,
MathMax,
MathMin,
NumberPrototypeToFixed,
SafePromiseAllReturnArrayLike,
RegExp,
RegExpPrototypeExec,
SafeMap,
SafePromiseAllReturnArrayLike,
StringPrototypePadStart,
StringPrototypePadEnd,
StringPrototypeRepeat,
Expand Down Expand Up @@ -316,6 +318,7 @@ const kSeparator = ' | ';

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

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

ArrayPrototypeForEach(ObjectKeys(summary.minimumCoverage), (key) => {
const { actual, expected, status } = summary.minimumCoverage[key]

Check failure on line 412 in lib/internal/test_runner/utils.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Missing semicolon
if (!status) {
report += `${red}${pad}${prefix}minimum coverage failed for ${key}. expected: ${expected} | actual: ${actual}\n${color}`;
}
});

report += `${prefix}end of coverage report\n`;
if (color) {
report += white;
Expand Down
Loading

0 comments on commit bb211c1

Please sign in to comment.