Skip to content

Commit

Permalink
test_runner: adds built in lcov reporter
Browse files Browse the repository at this point in the history
Fixes #49626

PR-URL: #50018
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
  • Loading branch information
philnash authored and targos committed Nov 11, 2023
1 parent d9f92bc commit c9b92bb
Show file tree
Hide file tree
Showing 7 changed files with 868 additions and 2 deletions.
20 changes: 18 additions & 2 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,18 @@ if (anAlwaysFalseCondition) {
}
```

### Coverage reporters

The tap and spec reporters will print a summary of the coverage statistics.
There is also an lcov reporter that will generate an lcov file which can be
used as an in depth coverage report.

```bash
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
```

### Limitations

The test runner's code coverage functionality has the following limitations,
which will be addressed in a future Node.js release:

Expand Down Expand Up @@ -850,6 +862,10 @@ The following built-reporters are supported:
* `junit`
The junit reporter outputs test results in a jUnit XML format

* `lcov`
The `lcov` reporter outputs test coverage when used with the
[`--experimental-test-coverage`][] flag.

When `stdout` is a [TTY][], the `spec` reporter is used by default.
Otherwise, the `tap` reporter is used by default.

Expand All @@ -861,11 +877,11 @@ to the test runner's output is required, use the events emitted by the
The reporters are available via the `node:test/reporters` module:

```mjs
import { tap, spec, dot, junit } from 'node:test/reporters';
import { tap, spec, dot, junit, lcov } from 'node:test/reporters';
```

```cjs
const { tap, spec, dot, junit } = require('node:test/reporters');
const { tap, spec, dot, junit, lcov } = require('node:test/reporters');
```

### Custom reporters
Expand Down
107 changes: 107 additions & 0 deletions lib/internal/test_runner/reporter/lcov.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
'use strict';

const { relative } = require('path');
const Transform = require('internal/streams/transform');

// This reporter is based on the LCOV format, as described here:
// https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
// Excerpts from this documentation are included in the comments that make up
// the _transform function below.
class LcovReporter extends Transform {
constructor(options) {
super({ ...options, writableObjectMode: true, __proto__: null });
}

_transform(event, _encoding, callback) {
if (event.type !== 'test:coverage') {
return callback(null);
}
let lcov = '';
// A tracefile is made up of several human-readable lines of text, divided
// into sections. If available, a tracefile begins with the testname which
// is stored in the following format:
// ## TN:\<test name\>
lcov += 'TN:\n';
const {
data: {
summary: { workingDirectory },
},
} = event;
try {
for (let i = 0; i < event.data.summary.files.length; i++) {
const file = event.data.summary.files[i];
// For each source file referenced in the .da file, there is a section
// containing filename and coverage data:
// ## SF:\<path to the source file\>
lcov += `SF:${relative(workingDirectory, file.path)}\n`;

// Following is a list of line numbers for each function name found in
// the source file:
// ## FN:\<line number of function start\>,\<function name\>
//
// After, there is a list of execution counts for each instrumented
// function:
// ## FNDA:\<execution count\>,\<function name\>
//
// This loop adds the FN lines to the lcov variable as it goes and
// gathers the FNDA lines to be added later. This way we only loop
// through the list of functions once.
let fnda = '';
for (let j = 0; j < file.functions.length; j++) {
const func = file.functions[j];
const name = func.name || `anonymous_${j}`;
lcov += `FN:${func.line},${name}\n`;
fnda += `FNDA:${func.count},${name}\n`;
}
lcov += fnda;

// This list is followed by two lines containing the number of
// functions found and hit:
// ## FNF:\<number of functions found\>
// ## FNH:\<number of function hit\>
lcov += `FNF:${file.totalFunctionCount}\n`;
lcov += `FNH:${file.coveredFunctionCount}\n`;

// Branch coverage information is stored which one line per branch:
// ## BRDA:\<line number\>,\<block number\>,\<branch number\>,\<taken\>
// Block number and branch number are gcc internal IDs for the branch.
// Taken is either '-' if the basic block containing the branch was
// never executed or a number indicating how often that branch was
// taken.
for (let j = 0; j < file.branches.length; j++) {
lcov += `BRDA:${file.branches[j].line},${j},0,${file.branches[j].count}\n`;
}

// Branch coverage summaries are stored in two lines:
// ## BRF:\<number of branches found\>
// ## BRH:\<number of branches hit\>
lcov += `BRF:${file.totalBranchCount}\n`;
lcov += `BRH:${file.coveredBranchCount}\n`;

// Then there is a list of execution counts for each instrumented line
// (i.e. a line which resulted in executable code):
// ## DA:\<line number\>,\<execution count\>[,\<checksum\>]
const sortedLines = file.lines.toSorted((a, b) => a.line - b.line);
for (let j = 0; j < sortedLines.length; j++) {
lcov += `DA:${sortedLines[j].line},${sortedLines[j].count}\n`;
}

// At the end of a section, there is a summary about how many lines
// were found and how many were actually instrumented:
// ## LH:\<number of lines with a non-zero execution count\>
// ## LF:\<number of instrumented lines\>
lcov += `LH:${file.coveredLineCount}\n`;
lcov += `LF:${file.totalLineCount}\n`;

// Each sections ends with:
// end_of_record
lcov += 'end_of_record\n';
}
} catch (error) {
return callback(error);
}
return callback(null, lcov);
}
}

module.exports = LcovReporter;
1 change: 1 addition & 0 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ const kBuiltinReporters = new SafeMap([
['dot', 'internal/test_runner/reporter/dot'],
['tap', 'internal/test_runner/reporter/tap'],
['junit', 'internal/test_runner/reporter/junit'],
['lcov', 'internal/test_runner/reporter/lcov'],
]);

const kDefaultReporter = process.stdout.isTTY ? 'spec' : 'tap';
Expand Down
10 changes: 10 additions & 0 deletions lib/test/reporters.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let dot;
let junit;
let spec;
let tap;
let lcov;

ObjectDefineProperties(module.exports, {
__proto__: null,
Expand Down Expand Up @@ -45,4 +46,13 @@ ObjectDefineProperties(module.exports, {
return tap;
},
},
lcov: {
__proto__: null,
configurable: true,
enumerable: true,
get() {
lcov ??= require('internal/test_runner/reporter/lcov');
return ReflectConstruct(lcov, arguments);
},
},
});
7 changes: 7 additions & 0 deletions test/fixtures/test-runner/output/lcov_reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';
require('../../../common');
const fixtures = require('../../../common/fixtures');
const spawn = require('node:child_process').spawn;

spawn(process.execPath,
['--no-warnings', '--experimental-test-coverage', '--test-reporter', 'lcov', fixtures.path('test-runner/output/output.js')], { stdio: 'inherit' });
Loading

0 comments on commit c9b92bb

Please sign in to comment.