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

7 files changed

Lines changed: 868 additions & 2 deletions

File tree

‎doc/api/test.md‎

Lines changed: 18 additions & 2 deletions
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
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict';
2+
3+
const{ relative }=require('path');
4+
constTransform=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+
classLcovReporterextendsTransform{
11+
constructor(options){
12+
super({ ...options,writableObjectMode: true,__proto__: null});
13+
}
14+
15+
_transform(event,_encoding,callback){
16+
if(event.type!=='test:coverage'){
17+
returncallback(null);
18+
}
19+
letlcov='';
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(leti=0;i<event.data.summary.files.length;i++){
32+
constfile=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+
letfnda='';
50+
for(letj=0;j<file.functions.length;j++){
51+
constfunc=file.functions[j];
52+
constname=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(letj=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+
constsortedLines=file.lines.toSorted((a,b)=>a.line-b.line);
85+
for(letj=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+
returncallback(error);
102+
}
103+
returncallback(null,lcov);
104+
}
105+
}
106+
107+
module.exports=LcovReporter;

‎lib/internal/test_runner/utils.js‎

Lines changed: 1 addition & 0 deletions
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
constkDefaultReporter=process.stdout.isTTY ? 'spec' : 'tap';

‎lib/test/reporters.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ let dot;
66
letjunit;
77
letspec;
88
lettap;
9+
letlcov;
910

1011
ObjectDefineProperties(module.exports,{
1112
__proto__: null,
@@ -45,4 +46,13 @@ ObjectDefineProperties(module.exports, {
4546
returntap;
4647
},
4748
},
49+
lcov: {
50+
__proto__: null,
51+
configurable: true,
52+
enumerable: true,
53+
get(){
54+
lcov??=require('internal/test_runner/reporter/lcov');
55+
returnReflectConstruct(lcov,arguments);
56+
},
57+
},
4858
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
require('../../../common');
3+
constfixtures=require('../../../common/fixtures');
4+
constspawn=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)