Skip to content

Commit e643786

Browse files
philnashUlisesGascon
authored andcommitted
test_runner: adds built in lcov reporter
Fixes #49626 PR-URL: #50018 Reviewed-By: Moshe Atlow <moshe@atlow.co.il> Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
1 parent 0b31183 commit e643786

File tree

7 files changed

+868
-2
lines changed

7 files changed

+868
-2
lines changed

doc/api/test.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,18 @@ if (anAlwaysFalseCondition) {
426426
}
427427
```
428428

429+
### Coverage reporters
430+
431+
The tap and spec reporters will print a summary of the coverage statistics.
432+
There is also an lcov reporter that will generate an lcov file which can be
433+
used as an in depth coverage report.
434+
435+
```bash
436+
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
437+
```
438+
439+
### Limitations
440+
429441
The test runner's code coverage functionality has the following limitations,
430442
which will be addressed in a future Node.js release:
431443

@@ -871,6 +883,10 @@ The following built-reporters are supported:
871883
* `junit`
872884
The junit reporter outputs test results in a jUnit XML format
873885

886+
* `lcov`
887+
The `lcov` reporter outputs test coverage when used with the
888+
[`--experimental-test-coverage`][] flag.
889+
874890
When `stdout` is a [TTY][], the `spec` reporter is used by default.
875891
Otherwise, the `tap` reporter is used by default.
876892

@@ -882,11 +898,11 @@ to the test runner's output is required, use the events emitted by the
882898
The reporters are available via the `node:test/reporters` module:
883899

884900
```mjs
885-
import { tap, spec, dot, junit } from 'node:test/reporters';
901+
import { tap, spec, dot, junit, lcov } from 'node:test/reporters';
886902
```
887903

888904
```cjs
889-
const { tap, spec, dot, junit } = require('node:test/reporters');
905+
const { tap, spec, dot, junit, lcov } = require('node:test/reporters');
890906
```
891907

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

lib/internal/test_runner/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ const kBuiltinReporters = new SafeMap([
118118
['dot', 'internal/test_runner/reporter/dot'],
119119
['tap', 'internal/test_runner/reporter/tap'],
120120
['junit', 'internal/test_runner/reporter/junit'],
121+
['lcov', 'internal/test_runner/reporter/lcov'],
121122
]);
122123

123124
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 ReflectConstruct(lcov, arguments);
56+
},
57+
},
4858
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
require('../../../common');
3+
const fixtures = require('../../../common/fixtures');
4+
const spawn = require('node:child_process').spawn;
5+
6+
spawn(process.execPath,
7+
['--no-warnings', '--experimental-test-coverage', '--test-reporter', 'lcov', fixtures.path('test-runner/output/output.js')], { stdio: 'inherit' });

0 commit comments

Comments
 (0)