Skip to content

Commit

Permalink
test_runner: add support for coverage thresholds
Browse files Browse the repository at this point in the history
  • Loading branch information
RedYetiDev committed Aug 17, 2024
1 parent 9e83853 commit 71a0be1
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 0 deletions.
42 changes: 42 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1084,13 +1084,18 @@ changes:
- v18.17.0
pr-url: https://github.com/nodejs/node/pull/47686
description: This option can be used with `--test`.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/54429
description: A value can now be specified for minimum coverage support.
-->

When used in conjunction with the `node:test` module, a code coverage report is
generated as part of the test runner output. If no tests are run, a coverage
report is not generated. See the documentation on
[collecting code coverage from tests][] for more details.

When a value is specified, it is treated as [`--test-coverage-min-line`][].

### `--experimental-test-module-mocks`

<!-- YAML
Expand Down Expand Up @@ -2232,6 +2237,39 @@ This option may be specified multiple times to include multiple glob patterns.
If both `--test-coverage-exclude` and `--test-coverage-include` are provided,
files must meet **both** criteria to be included in the coverage report.

### `--test-coverage-min-branch`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Require a minimum percent of covered branches. If code coverage does not reach
the threshold specified, the process will exit with code `1`.

### `--test-coverage-min-function`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Require a minimum percent of covered functions. If code coverage does not reach
the threshold specified, the process will exit with code `1`.

### `--test-coverage-min-line`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental
Require a minimum percent of covered lines. If code coverage does not reach
the threshold specified, the process will exit with code `1`.

### `--test-force-exit`

<!-- YAML
Expand Down Expand Up @@ -2999,6 +3037,9 @@ one is included in the list below.
* `--snapshot-blob`
* `--test-coverage-exclude`
* `--test-coverage-include`
* `--test-coverage-min-branch`
* `--test-coverage-min-function`
* `--test-coverage-min-line`
* `--test-name-pattern`
* `--test-only`
* `--test-reporter-destination`
Expand Down Expand Up @@ -3492,6 +3533,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`--print`]: #-p---print-script
[`--redirect-warnings`]: #--redirect-warningsfile
[`--require`]: #-r---require-module
[`--test-coverage-min-line`]: #--test-coverage-min-line
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
[`Buffer`]: buffer.md#class-buffer
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
Expand Down
9 changes: 9 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,15 @@ A glob pattern that excludes matching files from the coverage report
.It Fl -test-coverage-include
A glob pattern that only includes matching files in the coverage report
.
.It Fl -test-coverage-min-branch
Require a minimum threshold for branch coverage.
.
.It Fl -test-coverage-min-function
Require a minimum threshold for function coverage.
.
.It Fl -test-coverage-min-line
Require a minimum threshold for line coverage.
.
.It Fl -test-force-exit
Configures the test runner to exit the process once all known tests have
finished executing even if the event loop would otherwise remain active.
Expand Down
3 changes: 3 additions & 0 deletions lib/internal/test_runner/harness.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ function createTestTree(rootTestOptions, globalOptions) {
watching: false,
config: globalOptions,
coverage: null,
minLineCoverage: globalOptions.minimumLineCoveragePercent,
minBranchCoverage: globalOptions.minimumBranchCoveragePercent,
minFuncCoverage: globalOptions.minimumFunctionCoveragePercent,
resetCounters() {
harness.counters = {
__proto__: null,
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
FunctionPrototype,
MathMax,
Number,
NumberPrototypeToFixed,
ObjectDefineProperty,
ObjectSeal,
PromisePrototypeThen,
Expand Down Expand Up @@ -1002,6 +1003,21 @@ class Test extends AsyncResource {

if (coverage) {
reporter.coverage(nesting, loc, coverage);

if (coverage.totals.coveredLinePercent < harness.minLineCoverage) {
process.exitCode = 1;
reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(coverage.totals.coveredLinePercent, 2)}% line coverage does not meet threshold of ${NumberPrototypeToFixed(harness.minLineCoverage, 2)}%`);
}

if (coverage.totals.coveredBranchPercent < harness.minBranchCoverage) {
process.exitCode = 1;
reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(coverage.totals.coveredBranchPercent, 2)}% branch coverage does not meet threshold of ${NumberPrototypeToFixed(harness.minBranchCoverage, 2)}%`);
}

if (coverage.totals.coveredFunctionPercent < harness.minFuncCoverage) {
process.exitCode = 1;
reporter.diagnostic(nesting, loc, `Error: ${NumberPrototypeToFixed(coverage.totals.coveredFunctionPercent, 2)}% branch coverage does not meet threshold of ${NumberPrototypeToFixed(harness.minFuncCoverage, 2)}%`);
}
}

if (harness.watching) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ function parseCommandLine() {
let concurrency;
let coverageExcludeGlobs;
let coverageIncludeGlobs;
let minimumLineCoveragePercent;
let minimumBranchCoveragePercent;
let minimumFunctionCoveragePercent;
let destinations;
let only;
let reporters;
Expand Down Expand Up @@ -270,6 +273,9 @@ function parseCommandLine() {
if (coverage) {
coverageExcludeGlobs = getOptionValue('--test-coverage-exclude');
coverageIncludeGlobs = getOptionValue('--test-coverage-include');
minimumBranchCoveragePercent = getOptionValue('--test-coverage-min-branch');
minimumLineCoveragePercent = getOptionValue('--test-coverage-min-line');
minimumFunctionCoveragePercent = getOptionValue('--test-coverage-min-function');
}

const setup = reporterScope.bind(async (rootReporter) => {
Expand All @@ -290,6 +296,9 @@ function parseCommandLine() {
coverageIncludeGlobs,
destinations,
forceExit,
minimumBranchCoveragePercent,
minimumFunctionCoveragePercent,
minimumLineCoveragePercent,
only,
reporters,
setup,
Expand Down
19 changes: 19 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,25 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
AddOption("--experimental-test-coverage",
"enable code coverage in the test runner",
&EnvironmentOptions::test_runner_coverage);

AddOption("--experimental-test-coverage",
"enable code coverage in the test runner",
&EnvironmentOptions::test_runner_coverage);
AddOption("--test-coverage-min-branch",
"",
&EnvironmentOptions::test_runner_minimum_branch_coverage,
kAllowedInEnvvar);
AddOption("--test-coverage-min-function",
"",
&EnvironmentOptions::test_runner_minimum_branch_coverage,
kAllowedInEnvvar);
AddOption("--test-coverage-min-line",
"",
&EnvironmentOptions::test_runner_minimum_line_coverage,
kAllowedInEnvvar);
AddAlias("--experimental-test-coverage=",
{"--test-coverage-min-line", "--experimental-test-coverage"});

AddOption("--experimental-test-module-mocks",
"enable module mocking in the test runner",
&EnvironmentOptions::test_runner_module_mocks);
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ class EnvironmentOptions : public Options {
uint64_t test_runner_timeout = 0;
bool test_runner_coverage = false;
bool test_runner_force_exit = false;
uint64_t test_runner_minimum_branch_coverage = 0;
uint64_t test_runner_minimum_line_coverage = 0;
bool test_runner_module_mocks = false;
bool test_runner_snapshots = false;
bool test_runner_update_snapshots = false;
Expand Down
19 changes: 19 additions & 0 deletions test/parallel/test-runner-coverage.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,22 @@ test('coverage with included and excluded files', skipIfNoInspector, () => {
assert.strictEqual(result.status, 0);
assert(!findCoverageFileForPid(result.pid));
});

test('coverage with a minimum threshold', skipIfNoInspector, () => {
const fixture = fixtures.path('test-runner', 'coverage.js');
const args = [
'--experimental-test-coverage=80', '--test-reporter', 'tap',
fixture,
];
const result = spawnSync(process.execPath, args);
const report = getTapCoverageFixtureReport();

if (common.isWindows) {
return report.replaceAll('/', '\\');
}

assert(result.stdout.toString().includes(report));
assert(result.stdout.toString().includes('Error: 78.35% line coverage does not meet threshold of 80.00%'));
assert.strictEqual(result.status, 1);
assert(!findCoverageFileForPid(result.pid));
});

0 comments on commit 71a0be1

Please sign in to comment.