1- import { parseContent as parseContentClover } from "@cvrg-report/clover-json" ;
2- import { parseContent as parseContentCobertura } from "cobertura-parse" ;
3- import { parseContent as parseContentJacoco } from "@7sean68/jacoco-parse" ;
4- import { Section , source } from "lcov-parse" ;
5- import { OutputChannel } from "vscode" ;
1+ import { parseContent as parseContentClover } from "@cvrg-report/clover-json" ;
2+ import { parseContent as parseContentCobertura } from "cobertura-parse" ;
3+ import { parseContent as parseContentJacoco } from "@7sean68/jacoco-parse" ;
4+ import { Section , source } from "lcov-parse" ;
5+ import { OutputChannel } from "vscode" ;
66
7- import { CoverageFile , CoverageType } from "./coveragefile" ;
7+ import { CoverageFile , CoverageType } from "./coveragefile" ;
88
99export class CoverageParser {
1010 private outputChannel : OutputChannel ;
@@ -17,147 +17,289 @@ export class CoverageParser {
1717 * Extracts coverage sections of type xml and lcov
1818 * @param files array of coverage files in string format
1919 */
20- public async filesToSections ( files : Map < string , string > ) : Promise < Map < string , Section > > {
21- let coverages = new Map < string , Section > ( ) ;
22-
23- for ( const file of files ) {
24- const fileName = file [ 0 ] ;
25- const fileContent = file [ 1 ] ;
26-
27- // file is an array
28- let coverage = new Map < string , Section > ( ) ;
20+ public async filesToSections (
21+ files : Map < string , string >
22+ ) : Promise < Map < string , Section > > {
23+ const coverages = new Map < string , Section > ( ) ;
2924
25+ for ( const [ fileName , fileContent ] of files ) {
3026 // get coverage file type
3127 const coverageFile = new CoverageFile ( fileContent ) ;
3228 switch ( coverageFile . type ) {
3329 case CoverageType . CLOVER :
34- coverage = await this . xmlExtractClover ( fileName , fileContent ) ;
30+ await this . xmlExtractClover (
31+ coverages ,
32+ fileName ,
33+ fileContent
34+ ) ;
3535 break ;
3636 case CoverageType . JACOCO :
37- coverage = await this . xmlExtractJacoco ( fileName , fileContent ) ;
37+ await this . xmlExtractJacoco (
38+ coverages ,
39+ fileName ,
40+ fileContent
41+ ) ;
3842 break ;
3943 case CoverageType . COBERTURA :
40- coverage = await this . xmlExtractCobertura ( fileName , fileContent ) ;
44+ await this . xmlExtractCobertura (
45+ coverages ,
46+ fileName ,
47+ fileContent
48+ ) ;
4149 break ;
4250 case CoverageType . LCOV :
43- coverage = await this . lcovExtract ( fileName , fileContent ) ;
51+ this . lcovExtract ( coverages , fileName , fileContent ) ;
4452 break ;
4553 default :
4654 break ;
4755 }
48-
49- // add new coverage map to existing coverages generated so far
50- coverages = new Map ( [ ...coverages , ...coverage ] ) ;
5156 }
52-
5357 return coverages ;
5458 }
5559
56- private async convertSectionsToMap (
57- data : Section [ ] ,
58- ) : Promise < Map < string , Section > > {
59- const sections = new Map < string , Section > ( ) ;
60+ private async addSections (
61+ coverages : Map < string , Section > ,
62+ data : Section [ ]
63+ ) : Promise < void [ ] > {
6064 const addToSectionsMap = async ( section : Section ) => {
61- sections . set ( section . title + "::" + section . file , section ) ;
65+ const key = [ section . title , section . file ] . join ( "::" ) ;
66+ const existingSection = coverages . get ( key ) ;
67+
68+ if ( ! existingSection ) {
69+ coverages . set ( key , section ) ;
70+ return ;
71+ }
72+
73+ const mergedSection = this . mergeSections ( existingSection , section ) ;
74+ coverages . set ( key , mergedSection ) ;
6275 } ;
6376
6477 // convert the array of sections into an unique map
6578 const addPromises = data . map ( addToSectionsMap ) ;
66- await Promise . all ( addPromises ) ;
67- return sections ;
79+ return await Promise . all ( addPromises ) ;
6880 }
6981
70- private xmlExtractCobertura ( filename : string , xmlFile : string ) {
71- return new Promise < Map < string , Section > > ( ( resolve ) => {
82+ private xmlExtractCobertura (
83+ coverages : Map < string , Section > ,
84+ coverageFilename : string ,
85+ xmlFile : string
86+ ) {
87+ return new Promise < void > ( ( resolve ) => {
7288 const checkError = ( err : Error ) => {
7389 if ( err ) {
74- err . message = `filename: ${ filename } ${ err . message } ` ;
90+ err . message = `filename: ${ coverageFilename } ${ err . message } ` ;
7591 this . handleError ( "cobertura-parse" , err ) ;
76- return resolve ( new Map < string , Section > ( ) ) ;
92+ return resolve ( ) ;
7793 }
7894 } ;
7995
8096 try {
81- parseContentCobertura ( xmlFile , async ( err , data ) => {
82- checkError ( err ) ;
83- const sections = await this . convertSectionsToMap ( data ) ;
84- return resolve ( sections ) ;
85- } , true ) ;
86- // eslint-disable-next-line @typescript-eslint/no-explicit-any
97+ parseContentCobertura (
98+ xmlFile ,
99+ async ( err , data ) => {
100+ checkError ( err ) ;
101+ await this . addSections ( coverages , data ) ;
102+ return resolve ( ) ;
103+ } ,
104+ true
105+ ) ;
106+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87107 } catch ( error : any ) {
88108 checkError ( error ) ;
89109 }
90110 } ) ;
91111 }
92112
93- private xmlExtractJacoco ( filename : string , xmlFile : string ) {
94- return new Promise < Map < string , Section > > ( ( resolve ) => {
113+ private xmlExtractJacoco (
114+ coverages : Map < string , Section > ,
115+ coverageFilename : string ,
116+ xmlFile : string
117+ ) {
118+ return new Promise < void > ( ( resolve ) => {
95119 const checkError = ( err : Error ) => {
96120 if ( err ) {
97- err . message = `filename: ${ filename } ${ err . message } ` ;
121+ err . message = `filename: ${ coverageFilename } ${ err . message } ` ;
98122 this . handleError ( "jacoco-parse" , err ) ;
99- return resolve ( new Map < string , Section > ( ) ) ;
123+ return resolve ( ) ;
100124 }
101125 } ;
102126
103127 try {
104128 parseContentJacoco ( xmlFile , async ( err , data ) => {
105129 checkError ( err ) ;
106- const sections = await this . convertSectionsToMap ( data ) ;
107- return resolve ( sections ) ;
130+ await this . addSections ( coverages , data ) ;
131+ return resolve ( ) ;
108132 } ) ;
109- // eslint-disable-next-line @typescript-eslint/no-explicit-any
133+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110134 } catch ( error : any ) {
111135 checkError ( error ) ;
112136 }
113137 } ) ;
114138 }
115139
116- private async xmlExtractClover ( filename : string , xmlFile : string ) {
140+ private async xmlExtractClover (
141+ coverages : Map < string , Section > ,
142+ coverageFilename : string ,
143+ xmlFile : string
144+ ) {
117145 try {
118146 const data = await parseContentClover ( xmlFile ) ;
119- const sections = await this . convertSectionsToMap ( data ) ;
120- return sections ;
121- // eslint-disable-next-line @typescript-eslint/no-explicit-any
147+ await this . addSections ( coverages , data ) ;
148+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122149 } catch ( error : any ) {
123- error . message = `filename: ${ filename } ${ error . message } ` ;
150+ error . message = `filename: ${ coverageFilename } ${ error . message } ` ;
124151 this . handleError ( "clover-parse" , error ) ;
125- return new Map < string , Section > ( ) ;
126152 }
127153 }
128154
129- private lcovExtract ( filename : string , lcovFile : string ) {
130- return new Promise < Map < string , Section > > ( ( resolve ) => {
155+ private lcovExtract (
156+ coverages : Map < string , Section > ,
157+ coverageFilename : string ,
158+ lcovFile : string
159+ ) {
160+ return new Promise < void > ( ( resolve ) => {
131161 const checkError = ( err : Error ) => {
132162 if ( err ) {
133- err . message = `filename: ${ filename } ${ err . message } ` ;
163+ err . message = `filename: ${ coverageFilename } ${ err . message } ` ;
134164 this . handleError ( "lcov-parse" , err ) ;
135- return resolve ( new Map < string , Section > ( ) ) ;
165+ return resolve ( ) ;
136166 }
137167 } ;
138168
139169 try {
140170 source ( lcovFile , async ( err , data ) => {
141171 checkError ( err ) ;
142- const sections = await this . convertSectionsToMap ( data ) ;
143- return resolve ( sections ) ;
172+ await this . addSections ( coverages , data ) ;
173+ return resolve ( ) ;
144174 } ) ;
145- // eslint-disable-next-line @typescript-eslint/no-explicit-any
175+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146176 } catch ( error : any ) {
147177 checkError ( error ) ;
148178 }
149179 } ) ;
150180 }
151181
182+ private mergeSections ( existingSection : Section , section : Section ) : Section {
183+ const lines = this . mergeLineCoverage ( existingSection , section ) ;
184+ const branches = this . mergeBranchCoverage ( existingSection , section ) ;
185+
186+ return {
187+ ...existingSection ,
188+ lines,
189+ branches,
190+ } ;
191+ }
192+
193+ private mergeLineCoverage (
194+ existingCoverage : Section ,
195+ coverage : Section
196+ ) : Section [ "lines" ] {
197+ let hit = 0 ;
198+ let found = 0 ;
199+ const seen = new Set ( ) ;
200+ const hits = new Set (
201+ coverage . lines . details
202+ . filter ( ( { hit } ) => hit > 0 )
203+ . map ( ( { line } ) => line )
204+ ) ;
205+
206+ const details = existingCoverage . lines . details . map ( ( line ) => {
207+ found += 1 ;
208+ seen . add ( line . line ) ;
209+
210+ if ( hits . has ( line . line ) ) {
211+ line . hit += 1 ;
212+ }
213+
214+ if ( line . hit > 0 ) {
215+ hit += 1 ;
216+ }
217+
218+ return line ;
219+ } ) ;
220+
221+ coverage . lines . details
222+ . filter ( ( { line } ) => ! seen . has ( line ) )
223+ . map ( ( line ) => {
224+ found += 1 ;
225+
226+ if ( line . hit > 0 ) {
227+ hit += 1 ;
228+ }
229+
230+ details . push ( line ) ;
231+ } ) ;
232+
233+ return { details, hit, found } ;
234+ }
235+
236+ private mergeBranchCoverage (
237+ existingCoverage : Section ,
238+ coverage : Section
239+ ) : Section [ "branches" ] {
240+ if ( ! coverage . branches ) {
241+ return existingCoverage . branches ;
242+ }
243+ if ( ! existingCoverage . branches ) {
244+ return coverage . branches ;
245+ }
246+
247+ let hit = 0 ;
248+ let found = 0 ;
249+ const seen = new Set ( ) ;
250+
251+ const getKey = ( branch : {
252+ line : number ;
253+ block : number ;
254+ branch : number ;
255+ } ) => [ branch . line , branch . block , branch . branch ] . join ( ":" ) ;
256+
257+ const taken = new Set (
258+ coverage . branches . details
259+ . filter ( ( { taken } ) => taken > 0 )
260+ . map ( getKey )
261+ ) ;
262+
263+ const details = existingCoverage . branches . details . map ( ( branch ) => {
264+ const key = getKey ( branch ) ;
265+ found += 1 ;
266+ seen . add ( key ) ;
267+
268+ if ( taken . has ( key ) ) {
269+ branch . taken += 1 ;
270+ }
271+
272+ if ( branch . taken > 0 ) {
273+ hit += 1 ;
274+ }
275+
276+ return branch ;
277+ } ) ;
278+
279+ coverage . branches . details
280+ . filter ( ( branch ) => ! seen . has ( getKey ( branch ) ) )
281+ . map ( ( branch ) => {
282+ found += 1 ;
283+
284+ if ( branch . taken > 0 ) {
285+ hit += 1 ;
286+ }
287+
288+ details . push ( branch ) ;
289+ } ) ;
290+
291+ return { details, hit, found } ;
292+ }
293+
152294 private handleError ( system : string , error : Error ) {
153295 const message = error . message ? error . message : error ;
154296 const stackTrace = error . stack ;
155297 this . outputChannel . appendLine (
156- `[${ Date . now ( ) } ][coverageparser][${ system } ]: Error: ${ message } ` ,
298+ `[${ Date . now ( ) } ][coverageparser][${ system } ]: Error: ${ message } `
157299 ) ;
158300 if ( stackTrace ) {
159301 this . outputChannel . appendLine (
160- `[${ Date . now ( ) } ][coverageparser][${ system } ]: Stacktrace: ${ stackTrace } ` ,
302+ `[${ Date . now ( ) } ][coverageparser][${ system } ]: Stacktrace: ${ stackTrace } `
161303 ) ;
162304 }
163305 }
0 commit comments