Skip to content

Commit bc7dd15

Browse files
committed
Support importing a GNATcoverage report created outside the workspace
1 parent 2f6b8aa commit bc7dd15

File tree

1 file changed

+173
-11
lines changed

1 file changed

+173
-11
lines changed

integration/vscode/ada/src/gnatcov.ts

Lines changed: 173 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import assert from 'assert';
12
import { X2jOptions, XMLParser } from 'fast-xml-parser';
23
import * as fs from 'fs';
4+
import { cpus } from 'os';
35
import * as path from 'path';
46
import * as vscode from 'vscode';
57
import { CancellationToken } from 'vscode-languageclient';
8+
import { adaExtState } from './extension';
69
import { 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+
437599
export function convertSourceReport(
438600
data: source_type,
439601
token?: CancellationToken,

0 commit comments

Comments
 (0)