Skip to content

Commit 0a32c9f

Browse files
committed
test_runner: adds built in lcov reporter
Fixes nodejs#49626
1 parent 9c68320 commit 0a32c9f

File tree

7 files changed

+2183
-0
lines changed

7 files changed

+2183
-0
lines changed

doc/api/test.md

+16
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,18 @@ if (anAlwaysFalseCondition) {
402402
}
403403
```
404404

405+
### Coverage reporters
406+
407+
The tap and spec reporters will print a summary of the coverage statistics.
408+
There is also an lcov reporter that will generate an lcov file which can be
409+
used as an in depth coverage report.
410+
411+
```bash
412+
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
413+
```
414+
415+
### Limitations
416+
405417
The test runner's code coverage functionality has the following limitations,
406418
which will be addressed in a future Node.js release:
407419

@@ -635,6 +647,10 @@ The following built-reporters are supported:
635647
* `junit`
636648
The junit reporter outputs test results in a jUnit XML format
637649

650+
* `lcov`
651+
The `lcov` reporter outputs test coverage when used with the
652+
[`--experimental-test-coverage`][] flag.
653+
638654
When `stdout` is a [TTY][], the `spec` reporter is used by default.
639655
Otherwise, the `tap` reporter is used by default.
640656

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
const Transform = require('internal/streams/transform');
2+
3+
// This reporter is based on the LCOV format, as described here:
4+
// https://ltp.sourceforge.net/coverage/lcov/geninfo.1.php. Excerpts from this
5+
// documentation are included in the comments that make up the _transform
6+
// function below.
7+
class LcovReporter extends Transform {
8+
constructor(options) {
9+
super({ ...options, writableObjectMode: true });
10+
}
11+
12+
_transform(event, _encoding, callback) {
13+
if (event.type === 'test:coverage') {
14+
let lcov = '';
15+
// A tracefile is made up of several human-readable lines of text, divided
16+
// into sections. If available, a tracefile begins with the testname which
17+
// is stored in the following format:
18+
// ## TN:\<test name\>
19+
lcov += 'TN:\n';
20+
const {
21+
data: {
22+
summary: { workingDirectory },
23+
},
24+
} = event;
25+
try {
26+
for (let i = 0; i < event.data.summary.files.length; i++) {
27+
const file = event.data.summary.files[i];
28+
// For each source file referenced in the .da file, there is a section
29+
// containing filename and coverage data:
30+
// ## SF:\<path to the source file\>
31+
lcov += `SF:${file.path.replace(workingDirectory + '/', '')}\n`;
32+
33+
// Following is a list of line numbers for each function name found in
34+
// the source file:
35+
// ## FN:\<line number of function start\>,\<function name\>
36+
//
37+
// After, there is a list of execution counts for each instrumented
38+
// function:
39+
// ## FNDA:\<execution count\>,\<function name\>
40+
//
41+
// This loop adds the FN lines to the lcov variable as it goes and
42+
// gathers the FNDA lines to be added later. This way we only loop
43+
// through the list of functions once.
44+
let fnda = '';
45+
for (let j = 0; j < file.functions.length; j++) {
46+
const func = file.functions[j];
47+
const name = func.name || `anonymous_${j}`;
48+
lcov += `FN:${func.line},${name}\n`;
49+
fnda += `FNDA:${func.count},${name}\n`;
50+
}
51+
lcov += fnda;
52+
53+
// This list is followed by two lines containing the number of
54+
// functions found and hit:
55+
// ## FNF:\<number of functions found\>
56+
// ## FNH:\<number of function hit\>
57+
lcov += `FNF:${file.totalFunctionCount}\n`;
58+
lcov += `FNH:${file.coveredFunctionCount}\n`;
59+
60+
// Branch coverage information is stored which one line per branch:
61+
// ## BRDA:\<line number\>,\<block number\>,\<branch number\>,\<taken\>
62+
// Block number and branch number are gcc internal IDs for the branch.
63+
// Taken is either '-' if the basic block containing the branch was
64+
// never executed or a number indicating how often that branch was
65+
// taken.
66+
for (let j = 0; j < file.branches.length; j++) {
67+
lcov += `BRDA:${file.branches[j].line},${j},0,${file.branches[j].count}\n`;
68+
}
69+
70+
// Branch coverage summaries are stored in two lines:
71+
// ## BRF:\<number of branches found\>
72+
// ## BRH:\<number of branches hit\>
73+
lcov += `BRF:${file.totalBranchCount}\n`;
74+
lcov += `BRH:${file.coveredBranchCount}\n`;
75+
76+
// Then there is a list of execution counts for each instrumented line
77+
// (i.e. a line which resulted in executable code):
78+
// ## DA:\<line number\>,\<execution count\>[,\<checksum\>]
79+
const sortedLines = [...file.lines].sort((a, b) => a.line - b.line);
80+
for (let j = 0; j < sortedLines.length; j++) {
81+
lcov += `DA:${sortedLines[j].line},${sortedLines[j].count}\n`;
82+
}
83+
84+
// At the end of a section, there is a summary about how many lines
85+
// were found and how many were actually instrumented:
86+
// ## LH:\<number of lines with a non-zero execution count\>
87+
// ## LF:\<number of instrumented lines\>
88+
lcov += `LH:${file.coveredLineCount}\n`;
89+
lcov += `LF:${file.totalLineCount}\n`;
90+
91+
// Each sections ends with:
92+
// end_of_record
93+
lcov += 'end_of_record\n';
94+
}
95+
} catch (error) {
96+
callback(error);
97+
}
98+
return callback(null, lcov);
99+
}
100+
callback(null);
101+
}
102+
}
103+
104+
module.exports = LcovReporter;

lib/internal/test_runner/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ const kBuiltinReporters = new SafeMap([
112112
['dot', 'internal/test_runner/reporter/dot'],
113113
['tap', 'internal/test_runner/reporter/tap'],
114114
['junit', 'internal/test_runner/reporter/junit'],
115+
['lcov', 'internal/test_runner/reporter/lcov'],
115116
]);
116117

117118
const kDefaultReporter = process.stdout.isTTY ? 'spec' : 'tap';

lib/test/reporters.js

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let dot;
66
let junit;
77
let spec;
88
let tap;
9+
let lcov;
910

1011
ObjectDefineProperties(module.exports, {
1112
__proto__: null,
@@ -45,4 +46,13 @@ ObjectDefineProperties(module.exports, {
4546
return tap;
4647
},
4748
},
49+
lcov: {
50+
__proto__: null,
51+
configurable: true,
52+
enumerable: true,
53+
get() {
54+
lcov ??= require('internal/test_runner/reporter/lcov');
55+
return lcov;
56+
}
57+
}
4858
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
require('../../../common');
3+
const fixtures = require('../../../common/fixtures');
4+
const spawn = require('node:child_process').spawn;
5+
6+
const child = spawn(process.execPath,
7+
['--no-warnings', '--experimental-test-coverage', '--test-reporter', 'lcov', fixtures.path('test-runner/output/output.js')],
8+
{ stdio: 'pipe' });
9+
// eslint-disable-next-line no-control-regex
10+
child.stdout.on('data', (d) => process.stdout.write(d.toString().replace(/[^\x00-\x7F]/g, '').replace(/\u001b\[\d+m/g, '')));
11+
child.stderr.pipe(process.stderr);

0 commit comments

Comments
 (0)