diff --git a/src/test/coverage.ts b/src/test/coverage.ts index b76e2c3..4522125 100644 --- a/src/test/coverage.ts +++ b/src/test/coverage.ts @@ -1,4 +1,6 @@ import path from 'node:path'; +import cp from 'node:child_process'; +import { promisify } from 'node:util'; import { Location, Range, StatementCoverage, Uri } from 'vscode'; import { Context } from './testing'; import { Module, RootItem } from './item'; @@ -11,6 +13,17 @@ import { Module, RootItem } from './item'; * @returns Statement coverage information. */ export async function parseCoverage(context: Context, scope: RootItem, coverageFile: Uri) { + // Resolve GOROOT and GOMODCACHE + const { binPath } = context.go.settings.getExecutionCommand('go') || {}; + if (!binPath) { + throw new Error('Failed to run "go env" as the "go" binary cannot be found in either GOROOT or PATH'); + } + + const env = { + GOROOT: await getEnv(binPath, 'GOROOT'), + GOMODCACHE: await getEnv(binPath, 'GOMODCACHE'), + }; + const lines = Buffer.from(await context.workspace.fs.readFile(coverageFile)) .toString('utf-8') .split('\n'); @@ -24,7 +37,7 @@ export async function parseCoverage(context: Context, scope: RootItem, coverageF // the actual file path (either absolute or starting with .) // See https://golang.org/issues/40251. - const parse = parseLine(scope, line); + const parse = parseLine(env, scope, line); if (!parse) continue; const statements = coverage.get(`${parse.location.uri}`) || []; @@ -35,15 +48,25 @@ export async function parseCoverage(context: Context, scope: RootItem, coverageF return coverage; } +async function getEnv(binPath: string, name: string) { + const { stdout } = await promisify(cp.execFile)(binPath, ['env', name]); + return stdout.trim(); +} + // Derived from https://golang.org/cl/179377 +interface Env { + GOROOT: string; + GOMODCACHE: string; +} + /** * Parses a line in a coverage file. * @param scope - The module or workspace for resolving relative paths. * @param s - The line. * @returns The parsed line. */ -function parseLine(scope: RootItem, s: string) { +function parseLine(env: Env, scope: RootItem, s: string) { /** * Finds the last occurrence of {@link sep} in {@link s}, splits {@link s} * on that index, and returns the RHS parsed as a number. @@ -80,20 +103,36 @@ function parseLine(scope: RootItem, s: string) { return; } - // If it's a relative file path, convert it to an absolute path. From now - // on, we can assume that it's a real file name if it is an absolute path. - let filename = s; - if (filename.startsWith('.' + path.sep)) { - filename = path.resolve(filename, scope.dir.fsPath); + const filename = resolveCoveragePath(env, scope, s); + const range = new Range(startLine, startCol, endLine, endCol); + const location = new Location(Uri.file(filename), range); + return { location, count, statements }; +} + +function resolveCoveragePath(env: Env, scope: RootItem, filename: string) { + // If it's an absolute path, assume it's correct + if (path.isAbsolute(filename)) { + return filename; + } + + // If it's a relative path, convert it to an absolute path and return + if (filename.startsWith(`.${path.sep}`)) { + return path.resolve(filename, scope.dir.fsPath); } - // If the 'filename' is the package path + file, convert that to a real - // path, e.g. example.com/foo/bar.go -> /home/user/src/foo/bar.go. + // If the scope is a module and the file belongs to it, convert the filepath + // to a real path, e.g. example.com/foo/bar.go -> /home/user/src/foo/bar.go. if (scope instanceof Module && filename.startsWith(`${scope.path}/`)) { - filename = path.join(scope.dir.fsPath, filename.substring(scope.path.length + 1)); + return path.join(scope.dir.fsPath, filename.substring(scope.path.length + 1)); } - const range = new Range(startLine, startCol, endLine, endCol); - const location = new Location(Uri.file(filename), range); - return { location, count, statements }; + // If the first segment of the path contains a dot, assume it's a module + const [first] = filename.split(/\\|\//); + if (first.includes('.')) { + // TODO: Resolve the version + return path.join(env.GOMODCACHE, filename); + } + + // If the first segment does not contain a dot, assume it's a stdlib package + return path.join(env.GOROOT, 'src', filename); }