1+ import assert from 'assert' ;
12import { X2jOptions , XMLParser } from 'fast-xml-parser' ;
23import * as fs from 'fs' ;
4+ import { cpus } from 'os' ;
35import * as path from 'path' ;
46import * as vscode from 'vscode' ;
57import { CancellationToken } from 'vscode-languageclient' ;
8+ import { adaExtState } from './extension' ;
69import { parallelize , staggerProgress } from './helpers' ;
7- import { cpus } from 'os' ;
8- import { assert } from 'console' ;
910
1011/**
1112 * TypeScript types to represent data from GNATcoverage XML reports
@@ -299,25 +300,109 @@ export async function addCoverageData(run: vscode.TestRun, covDir: string) {
299300 progress . report ( {
300301 message : `${ done } / ${ totalFiles } source files` ,
301302 } ) ;
303+
304+ let posixForeignPrefix : string | undefined ;
305+ let posixLocalPrefix : string | undefined ;
306+
307+ const procs = process . env . PROCESSORS ? Number ( process . env . PROCESSORS ) : 0 ;
302308 const fileCovs = (
303309 await parallelize (
304310 array ,
305- Math . min ( cpus ( ) . length , 8 ) ,
311+ Math . min ( procs == 0 ? cpus ( ) . length : procs , 8 ) ,
306312 async ( file ) => {
307313 if ( token . isCancellationRequested ) {
308314 throw new vscode . CancellationError ( ) ;
309315 }
310316
311317 assert ( file [ '@_name' ] ) ;
318+ const foreignPath = file [ '@_name' ] ;
319+ /**
320+ * The foreign machine may have a different path
321+ * format, so we normalize to POSIX which is valid on
322+ * both Windows and POSIX OS-es.
323+ */
324+ const posixForeignPath = toPosix ( foreignPath ) ;
325+
326+ let srcUri : vscode . Uri | undefined = undefined ;
327+
328+ /**
329+ * The goal here is to find the file in the workspace
330+ * corresponding to the name attribute in the GNATcov
331+ * report.
332+ *
333+ * The name could be a basename (older GNATcov
334+ * versions) or an absolute path (newer GNATcov
335+ * versions).
336+ *
337+ * In the case of a basename, the only course of action
338+ * is to do a file lookup in the workspace.
339+ *
340+ * In the case of an absolute path, the path
341+ * corresponds to the machine where the report was
342+ * created which might be a foreign machine or the
343+ * local host. We can't know in which situation we are
344+ * so it's best to assume that it's a foreign machine.
345+ *
346+ * Then the logic consists of searching for the first
347+ * file by basename, which gives a local absolute path.
348+ * Then by comparing the local absolute path and the
349+ * foreign absolute path, we can find a foreign prefix
350+ * path that should be mapped to the local prefix path
351+ * to compute local absolute paths. Subsequent files
352+ * can use the computed prefixes directly without a
353+ * workspace lookup.
354+ */
312355
313- let srcUri : vscode . Uri ;
314- if ( path . isAbsolute ( file [ '@_name' ] ! ) ) {
315- srcUri = vscode . Uri . file ( file [ '@_name' ] ! ) ;
316- } else {
356+ if ( path . posix . isAbsolute ( posixForeignPath ) ) {
357+ let localFullPath ;
358+ if ( posixLocalPrefix && posixForeignPrefix ) {
359+ /**
360+ * The prefixes have already been determined, so
361+ * use them directly.
362+ */
363+
364+ if ( posixForeignPath . startsWith ( posixForeignPrefix ) ) {
365+ // Extract the relative path based on the foreign prefix
366+ const posixForeignRelPath = path . relative (
367+ posixForeignPrefix ,
368+ posixForeignPath ,
369+ ) ;
370+
371+ // Resolve the relative path with the local prefix
372+ localFullPath = path . join (
373+ posixLocalPrefix ,
374+ posixForeignRelPath ,
375+ ) ;
376+ }
377+ }
378+
379+ // Fallback to using the input path as is
380+ localFullPath = localFullPath ?? foreignPath ;
381+
382+ if ( fs . existsSync ( localFullPath ) ) {
383+ srcUri = vscode . Uri . file ( localFullPath ) ;
384+ }
385+ }
386+
387+ if ( srcUri === undefined ) {
388+ /**
389+ * If the prefixes haven't been found yet, or
390+ * the last prefixes used were not successful,
391+ * try a workspace lookup of the basename.
392+ */
317393 const found = await vscode . workspace . findFiles (
318- `**/${ file [ '@_name' ] ! } ` ,
319- null ,
394+ `**/${ path . posix . basename ( posixForeignPath ) } ` ,
395+ /**
396+ * Avoid searching in the object dir because we
397+ * might land on gnatcov-instrumented versions
398+ * of the sources.
399+ */
400+ `${ await adaExtState
401+ . getObjectDir ( )
402+ . then ( ( objDir ) => `${ objDir } /**/*` )
403+ . catch ( ( ) => null ) } `,
320404 1 ,
405+ token ,
321406 ) ;
322407 if ( found . length == 0 ) {
323408 return undefined ;
@@ -326,6 +411,61 @@ export async function addCoverageData(run: vscode.TestRun, covDir: string) {
326411 srcUri = found [ 0 ] ;
327412 }
328413
414+ if (
415+ posixForeignPrefix === undefined &&
416+ posixLocalPrefix === undefined &&
417+ path . posix . isAbsolute ( posixForeignPath )
418+ ) {
419+ /**
420+ * If the prefixes haven't been calculated, and the
421+ * foreign path is absolute, let's try to compute
422+ * the prefixes based on the workspace URI that was
423+ * found.
424+ */
425+
426+ const localAbsPath = srcUri . fsPath ;
427+ const posixLocalAbsPath = toPosix ( localAbsPath ) ;
428+
429+ /**
430+ * Find the longest common prefix between both
431+ * paths by starting to compare the characters
432+ * from the end of each string and iterating
433+ * backwards.
434+ */
435+ let revIndex = 0 ;
436+ while (
437+ revIndex < posixForeignPath . length &&
438+ revIndex < posixLocalAbsPath . length &&
439+ posixForeignPath [ posixForeignPath . length - revIndex ] ==
440+ posixLocalAbsPath [ posixLocalAbsPath . length - revIndex ]
441+ ) {
442+ revIndex ++ ;
443+ }
444+
445+ // Now the index points to the first different
446+ // character, so move it back to the last identical
447+ // character to make the slice operations below
448+ // more natural.
449+ revIndex -- ;
450+
451+ if (
452+ revIndex < posixForeignPath . length &&
453+ revIndex < posixLocalAbsPath . length
454+ ) {
455+ posixLocalPrefix = posixLocalAbsPath . slice (
456+ 0 ,
457+ posixLocalAbsPath . length - revIndex ,
458+ ) ;
459+ posixForeignPrefix = posixForeignPath . slice (
460+ 0 ,
461+ posixForeignPath . length - revIndex ,
462+ ) ;
463+ } else {
464+ // Could not find a common prefix so don't
465+ // do anything
466+ }
467+ }
468+
329469 const total = file . metric . find (
330470 ( m ) => m [ '@_kind' ] == 'total_lines_of_relevance' ,
331471 ) ! [ '@_count' ] ;
@@ -334,14 +474,15 @@ export async function addCoverageData(run: vscode.TestRun, covDir: string) {
334474 ] ;
335475
336476 const fileReportBasename = data . coverage_report . sources ! [ 'xi:include' ] . find (
337- ( inc ) => inc [ '@_href' ] == `${ path . basename ( file [ '@_name' ] ! ) } .xml` ,
477+ ( inc ) =>
478+ inc [ '@_href' ] == `${ path . posix . basename ( posixForeignPath ) } .xml` ,
338479 ) ! [ '@_href' ] ;
339480 const fileReportPath = path . join ( covDir , fileReportBasename ) ;
340481
341482 if ( covered > total ) {
342483 throw Error (
343484 `Got ${ covered } covered lines for a` +
344- ` total of ${ total } in ${ file [ '@_name' ] ! } ` ,
485+ ` total of ${ total } in ${ file [ '@_name' ] } ` ,
345486 ) ;
346487 }
347488
@@ -434,6 +575,27 @@ export class GnatcovFileCoverage extends vscode.FileCoverage {
434575 }
435576}
436577
578+ /**
579+ *
580+ * @param p - a path
581+ * @returns a POSIX version of the same path obtained by replacing occurences
582+ * of `\` with `/`. If the input path was a Windows absolute path, a `/` is
583+ * prepended to the output to make it also an absolute path.
584+ */
585+ function toPosix ( p : string ) {
586+ let posixPath = p . replace ( RegExp ( `\\${ path . win32 . sep } ` , 'g' ) , path . posix . sep ) ;
587+
588+ /**
589+ * If it was an absolute path from Windows, we have to
590+ * manually make it a POSIX absolute path.
591+ */
592+ if ( path . win32 . isAbsolute ( p ) && ! path . posix . isAbsolute ( posixPath ) ) {
593+ posixPath = `/${ posixPath } ` ;
594+ }
595+
596+ return posixPath ;
597+ }
598+
437599export function convertSourceReport (
438600 data : source_type ,
439601 token ?: CancellationToken ,
0 commit comments