11import { X2jOptions , XMLParser } from 'fast-xml-parser' ;
2- import { number } from 'fp-ts' ;
32import * as fs from 'fs' ;
3+ import * as path from 'path' ;
4+ import * as vscode from 'vscode' ;
5+ import { CancellationToken } from 'vscode-languageclient' ;
46
57/**
68 * TypeScript types to represent data from GNATcoverage XML reports
@@ -56,7 +58,7 @@ type xi_include_type = {
5658 '@_href' : string ;
5759} ;
5860
59- type source_type = {
61+ export type source_type = {
6062 '@_file' : string ;
6163 '@_coverage_level' : coverage_level_type ;
6264 scope_metric : scope_metric_type [ ] ;
@@ -94,7 +96,7 @@ type obligation_stats_type = {
9496 metric : metric_type [ ] ;
9597 '@_kind' : string ;
9698} ;
97- type src_mapping_type = {
99+ export type src_mapping_type = {
98100 src : src_type ;
99101 statement : statement_type [ ] ;
100102 decision : decision_type [ ] ;
@@ -104,7 +106,7 @@ type src_mapping_type = {
104106type src_type = {
105107 line : line_type [ ] ;
106108} ;
107- type line_type = {
109+ export type line_type = {
108110 '@_num' : number ;
109111 '@_src' : string ;
110112 '@_column_begin' : number ;
@@ -142,7 +144,20 @@ type statement_type = {
142144 * https://docs.adacore.com/gnatcoverage-docs/html/gnatcov/exemptions.html#reporting-about-coverage-exemptions
143145 * for more information.
144146 */
145- type coverage_type = '.' | '+' | '-' | '!' | '?' | '#' | '@' | '*' | '0' | 'v' | '>' ;
147+ export const coverage_type_values = [
148+ '.' ,
149+ '+' ,
150+ '-' ,
151+ '!' ,
152+ '?' ,
153+ '#' ,
154+ '@' ,
155+ '*' ,
156+ '0' ,
157+ 'v' ,
158+ '>' ,
159+ ] as const ;
160+ export type coverage_type = ( typeof coverage_type_values ) [ number ] ;
146161
147162type decision_type = {
148163 src ?: src_type ;
@@ -256,3 +271,164 @@ export function parseGnatcovFileXml(path: string): source_type {
256271 throw Error ( `Could not parse GNATcoverage report: ${ path } ` ) ;
257272 }
258273}
274+
275+ /**
276+ *
277+ * @param run - the test run to add coverage data to
278+ * @param covDir - The path to the directory containing GNATcoverage XML
279+ * reports. The directory must contain a single 'index.xml' file.
280+ */
281+ export async function addCoverageData ( run : vscode . TestRun , covDir : string ) {
282+ const indexPath = path . join ( covDir , 'index.xml' ) ;
283+ const data = parseGnatcovIndexXml ( indexPath ) ;
284+
285+ await vscode . window . withProgress (
286+ {
287+ cancellable : true ,
288+ location : vscode . ProgressLocation . Notification ,
289+ title : 'Loading GNATcoverage report' ,
290+ } ,
291+ async ( progress , token ) => {
292+ const array = data . coverage_report . coverage_summary ! . file ;
293+ let done : number = 0 ;
294+ const totalFiles = array . length ;
295+ for ( const file of array ) {
296+ if ( token . isCancellationRequested ) {
297+ throw new vscode . CancellationError ( ) ;
298+ }
299+
300+ const found = await vscode . workspace . findFiles ( `**/${ file [ '@_name' ] ! } ` , null , 1 ) ;
301+ if ( found . length == 0 ) continue ;
302+
303+ const srcUri = found [ 0 ] ;
304+ const total = file . metric . find ( ( m ) => m [ '@_kind' ] == 'total_lines_of_relevance' ) ! [
305+ '@_count'
306+ ] ;
307+ const covered = file . metric . find ( ( m ) => m [ '@_kind' ] == 'fully_covered' ) ! [ '@_count' ] ;
308+
309+ const fileReportBasename = data . coverage_report . sources ! [ 'xi:include' ] . find (
310+ ( inc ) => inc [ '@_href' ] == `${ file [ '@_name' ] ! } .xml` ,
311+ ) ! [ '@_href' ] ;
312+ const fileReportPath = path . join ( covDir , fileReportBasename ) ;
313+
314+ if ( covered > total ) {
315+ throw Error (
316+ `Got ${ covered } covered lines for a` +
317+ ` total of ${ total } in ${ file [ '@_name' ] ! } ` ,
318+ ) ;
319+ }
320+
321+ const fileCov = new GnatcovFileCoverage ( fileReportPath , srcUri , { covered, total } ) ;
322+ run . addCoverage ( fileCov ) ;
323+
324+ progress . report ( {
325+ message : `${ ++ done } / ${ totalFiles } source files` ,
326+ increment : ( 100 * 1 ) / totalFiles ,
327+ } ) ;
328+ }
329+ } ,
330+ ) ;
331+ }
332+ /**
333+ * A class that holds the summary coverage data of a source file, and provides
334+ * a method to load detailed coverage data about that source file.
335+ */
336+ export class GnatcovFileCoverage extends vscode . FileCoverage {
337+ sourceFileXmlReport : string ;
338+
339+ /**
340+ *
341+ * @param detailedXmlReportPath - The path to the GNATcov XML report for the given source file.
342+ * @param uri - URI of the source file
343+ * @param statementCoverage - statement coverage information
344+ * @param branchCoverage - branch coverage information, if available
345+ * @param declarationCoverage - declaration coverage information, if available
346+ */
347+ constructor (
348+ detailedXmlReportPath : string ,
349+ uri : vscode . Uri ,
350+ statementCoverage : vscode . TestCoverageCount ,
351+ branchCoverage ?: vscode . TestCoverageCount ,
352+ declarationCoverage ?: vscode . TestCoverageCount ,
353+ ) {
354+ super ( uri , statementCoverage , branchCoverage , declarationCoverage ) ;
355+ this . sourceFileXmlReport = detailedXmlReportPath ;
356+ }
357+
358+ /**
359+ * Report detailed coverage information by loading the file's GNATcov XML report.
360+ */
361+ public async load ( token ?: CancellationToken ) : Promise < vscode . FileCoverageDetail [ ] > {
362+ const data = parseGnatcovFileXml ( this . sourceFileXmlReport ) ;
363+ return Promise . resolve ( convertSourceReport ( data , token ) ) ;
364+ }
365+ }
366+
367+ export function convertSourceReport (
368+ data : source_type ,
369+ token ?: CancellationToken ,
370+ ) : vscode . StatementCoverage [ ] {
371+ return data . src_mapping . flatMap ( ( src_mapping ) => convertSrcMapping ( src_mapping , token ) ) ;
372+ }
373+
374+ export function convertSrcMapping (
375+ src_mapping : src_mapping_type ,
376+ token ?: CancellationToken ,
377+ ) : vscode . StatementCoverage [ ] {
378+ return src_mapping . src . line
379+ . map ( ( line ) => {
380+ if ( token ?. isCancellationRequested ) {
381+ throw new vscode . CancellationError ( ) ;
382+ }
383+
384+ const zeroBasedLine = line [ '@_num' ] - 1 ;
385+ const lineLength = line [ '@_src' ] . length ;
386+ const res = new vscode . StatementCoverage (
387+ false ,
388+ new vscode . Range ( zeroBasedLine , 0 , zeroBasedLine , lineLength ) ,
389+ ) ;
390+
391+ switch ( src_mapping [ '@_coverage' ] ) {
392+ case '-' :
393+ case '!' :
394+ case '?' :
395+ case '+' :
396+ /**
397+ * Lines with coverage obligations are highlighted
398+ * as covered iff they are marked as '+'
399+ */
400+ res . executed = src_mapping [ '@_coverage' ] == '+' ;
401+ return res ;
402+
403+ case '.' :
404+ /**
405+ * Lines with no coverage obligations are not highlighted
406+ */
407+ return undefined ;
408+
409+ case '#' :
410+ case '@' :
411+ case '*' :
412+ /**
413+ * Exempted lines are not highlighted
414+ */
415+ return undefined ;
416+
417+ case 'v' :
418+ case '>' :
419+ /**
420+ * Symbols are for object level coverage which
421+ * is not supported in VS Code.
422+ */
423+ return undefined ;
424+
425+ case '0' :
426+ /**
427+ * Symbol specific to binary traces which are
428+ * not used in the VS Code integration.
429+ */
430+ return undefined ;
431+ }
432+ } )
433+ . filter ( ( v ) => ! ! v ) ;
434+ }
0 commit comments