Skip to content

Commit

Permalink
test_runner: feat add coverage threshold for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pulkit-30 committed Dec 19, 2023
1 parent 14e3444 commit bfcfe72
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 55 deletions.
11 changes: 11 additions & 0 deletions index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { spec } from 'node:test/reporters';
import { run } from 'node:test';
import process from 'node:process';

run({ files: ['./index.test.mjs'], coverage: {
lines: 100,
branches: 100,
functions: 100,
} })
.compose(spec)
.pipe(process.stdout);
6 changes: 6 additions & 0 deletions index.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import assert from 'node:assert';
import test from 'node:test';

test('hello test', () => {
// throw new Error("error");
});
23 changes: 20 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 kDefaultMinimumThreshold = {
__proto__: null,
lines: 0,
functions: 0,
branches: 0,
};

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

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

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

coverageSummary.threshold = {
__proto__: null,
lines: doesThresholdPass(this.minimumThreshold.lines, coverageSummary.totals.coveredLinePercent),
functions: doesThresholdPass(this.minimumThreshold.functions, coverageSummary.totals.coveredFunctionPercent),
branches: doesThresholdPass(this.minimumThreshold.branches, coverageSummary.totals.coveredBranchPercent),
};
return coverageSummary;
}

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

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

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

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

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

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

module.exports = { setupCoverage, TestCoverage };
17 changes: 13 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,20 @@ function createProcessEventHandler(eventName, rootTest) {
}

function configureCoverage(rootTest, globalOptions) {
if (!globalOptions.coverage) {
return null;
let coverageThreshold = rootTest.coverage;
if (!coverageThreshold) {
if (!globalOptions.coverage) {
return null;
}
const { 1: value } = StringPrototypeSplit(
ArrayPrototypeFilter(process.execArgv, (arg) =>
StringPrototypeStartsWith(arg, '--experimental-test-coverage')), '=');
coverageThreshold = { __proto__: null, lines: value, functions: value, branches: value };
}

const { setupCoverage } = require('internal/test_runner/coverage');

try {
return setupCoverage();
return setupCoverage(coverageThreshold);
} 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
20 changes: 17 additions & 3 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';
const {
ArrayPrototypeFilter,
ArrayPrototypeJoin,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeReduce,
Expand All @@ -10,6 +12,8 @@ const {
FunctionPrototype,
MathMax,
Number,
ObjectDefineProperty,
ObjectKeys,
ObjectSeal,
PromisePrototypeThen,
PromiseResolve,
Expand All @@ -21,7 +25,6 @@ const {
SafePromiseAll,
SafePromiseRace,
SymbolDispose,
ObjectDefineProperty,
Symbol,
} = primordials;
const { getCallerLocation } = internalBinding('util');
Expand Down Expand Up @@ -49,6 +52,7 @@ const {
once: runOnce,
} = require('internal/util');
const { isPromise } = require('internal/util/types');
const console = require('internal/console/global');
const {
validateAbortSignal,
validateNumber,
Expand Down Expand Up @@ -209,7 +213,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 +243,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 +263,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 @@ -670,7 +676,9 @@ class Test extends AsyncResource {
// postRun() method is called when the process is getting ready to exit.
// This helps catch any asynchronous activity that occurs after the tests
// have finished executing.
this.postRun();
try {
this.postRun();
} catch { /* ignore error */ }
}
}

Expand Down Expand Up @@ -754,6 +762,12 @@ class Test extends AsyncResource {

if (coverage) {
reporter.coverage(nesting, loc, coverage);
const failedCoverage = ArrayPrototypeFilter(
ObjectKeys(coverage.threshold), (key) => !coverage.threshold[key]);
if (failedCoverage.length > 0) {
console.error(`test coverage failed for ${ArrayPrototypeJoin(failedCoverage, ', ')}`);
process.exit(1);
}
}

reporter.end();
Expand Down
11 changes: 10 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,12 @@ 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.threshold), (key) => {
if (!summary.threshold[key]) {
report += `${red}${prefix}coverage threshold for ${key} not met\n${color}`;
}
});

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

0 comments on commit bfcfe72

Please sign in to comment.