src/goCover.ts: handle Windows file system and fix profile parsing
The first element of code coverage profile could be one of
<import_path>/<go_filename> or
<absolute_path_to_dir>/<go_filename> or
./<go_filename>
We considered the first case, but not the other cases.
Handle the other cases.
This CL also addresses bugs in Windows.
- The file path can include ':' so, parsing `go list` output
base on ':' does not work on Windows. Instead, we refactored
getNonVendorPackages that does the same job, created a new
getImportPathToFolder, and changed to use it.
- While refactoring, removed the code to support go versions
older than 1.9 - whose 'go list' results include vendored packages.
- Fixed the cover tests to use the correct separator.
And, refactored the code in a way to avoid global variables
if possible.
Update golang/vscode-go#239.
Update #40251.
Change-Id: I60d2466c91aba95b4cb01597ad9d6ed7f9f36230
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/243177
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/src/goCover.ts b/src/goCover.ts
index 2fa1c92..b1d3994 100644
--- a/src/goCover.ts
+++ b/src/goCover.ts
@@ -5,15 +5,13 @@
'use strict';
-import cp = require('child_process');
import fs = require('fs');
import path = require('path');
-import util = require('util');
import vscode = require('vscode');
import { isModSupported } from './goModules';
+import { getImportPathToFolder } from './goPackages';
import { getTestFlags, goTest, showTestOutput, TestConfig } from './testUtils';
-import { getBinPath, getCurrentGoPath, getGoConfig, getWorkspaceFolderPath } from './util';
-import { envPath } from './utils/goPath';
+import { getGoConfig } from './util';
let gutterSvgs: { [key: string]: string };
let decorators: {
@@ -31,6 +29,7 @@
coveredGutterStyle: string;
uncoveredGutterStyle: string;
};
+
// a list of modified, unsaved go files with actual code edits (rather than comment edits)
let modifiedFiles: {
[key: string]: boolean;
@@ -155,18 +154,14 @@
coveredRange: vscode.Range[];
}
-let coverageFiles: { [key: string]: CoverageData } = {};
-let coveragePath = new Map<string, CoverageData>();
-let pathsToDirs = new Map<string, string>();
+let coverageData: { [key: string]: CoverageData } = {}; // actual file path to the coverage data.
let isCoverageApplied: boolean = false;
/**
* Clear the coverage on all files
*/
function clearCoverage() {
- coverageFiles = {};
- coveragePath = new Map<string, CoverageData>();
- pathsToDirs = new Map<string, string>();
+ coverageData = {};
disposeDecorators();
isCoverageApplied = false;
}
@@ -180,6 +175,8 @@
export function applyCodeCoverageToAllEditors(coverProfilePath: string, testDir?: string): Promise<void> {
const v = new Promise<void>((resolve, reject) => {
try {
+ const coveragePath = new Map<string, CoverageData>(); // <filename> from the cover profile to the coverage data.
+
// Clear existing coverage files
clearCoverage();
@@ -188,15 +185,22 @@
// for now read synchronously and hope for no errors
const contents = fs.readFileSync(coverProfilePath).toString();
contents.split('\n').forEach((line) => {
+ // go test coverageprofile generates output:
+ // filename:StartLine.StartColumn,EndLine.EndColumn Hits CoverCount
+ // where the filename is either the import path + '/' + base file name, or
+ // the actual file path (either absolute or starting with .)
+ // See https://golang.org/issues/40251.
+ //
+ // The first line will be like "mode: set" which we will ignore.
const parse = line.match(/([^:]+)\:([\d]+)\.([\d]+)\,([\d]+)\.([\d]+)\s([\d]+)\s([\d]+)/);
if (!parse) { return; }
- const lastSlash = parse[1].lastIndexOf('/'); // ok for windows?
+ const lastSlash = parse[1].lastIndexOf('/');
if (lastSlash !== -1) {
seenPaths.add(parse[1].slice(0, lastSlash));
}
// and fill in coveragePath
- const coverage = getPathData(parse[1]);
+ const coverage = coveragePath.get(parse[1]) || { coveredRange: [], uncoveredRange: [] };
const range = new vscode.Range(
// Start Line converted to zero based
parseInt(parse[2], 10) - 1,
@@ -213,11 +217,12 @@
} else {
coverage.uncoveredRange.push(range);
}
- setPathData(parse[1], coverage);
+ coveragePath.set(parse[1], coverage);
});
- const pathPromise = getPathsToDirs(seenPaths, pathsToDirs, testDir);
- pathPromise.then(() => {
- createCoverageData();
+
+ getImportPathToFolder([...seenPaths], testDir)
+ .then((pathsToDirs) => {
+ createCoverageData(pathsToDirs, coveragePath);
setDecorators();
vscode.window.visibleTextEditors.forEach(applyCodeCoverage);
resolve();
@@ -230,47 +235,29 @@
return v;
}
-/**
- * Get the object that holds the coverage data for given file path.
- * @param filePath
- */
-function getCoverageData(filePath: string): CoverageData {
- if (filePath.startsWith('_')) {
- filePath = filePath.substr(1);
- }
- if (process.platform === 'win32') {
- const parts = filePath.split('/');
- if (parts.length) {
- filePath = parts.join(path.sep);
- }
- }
- return coverageFiles[filePath] || { coveredRange: [], uncoveredRange: [] };
-}
+function createCoverageData(
+ pathsToDirs: Map<string, string>,
+ coveragePath: Map<string, CoverageData>) {
-/**
- * Get the CoverageData for an import path.
- * @param importPath
- */
-function getPathData(importPath: string): CoverageData {
- return coveragePath.get(importPath) || { coveredRange: [], uncoveredRange: [] };
-}
-
-/**
- * Set the CoverageData for an import path.
- * @param importPath
- * @param data
- */
-function setPathData(importPath: string, data: CoverageData) {
- coveragePath.set(importPath, data);
-}
-
-function createCoverageData() {
coveragePath.forEach((cd, ip) => {
const lastSlash = ip.lastIndexOf('/');
+ if (lastSlash === -1) { // malformed
+ console.log(`invalid entry: ${ip}`);
+ return;
+ }
const importPath = ip.slice(0, lastSlash);
- const fileDir = pathsToDirs.get(importPath);
- const file = fileDir + ip.slice(lastSlash); // what about Windows?
- setCoverageData(file, cd);
+ let fileDir = importPath;
+ if (path.isAbsolute(importPath)) {
+ // This is the true file path.
+ } else if (importPath.startsWith('.')) {
+ fileDir = path.resolve(fileDir);
+ } else {
+ // This is the package import path.
+ // we need to look up `go list` output stored in pathsToDir.
+ fileDir = pathsToDirs.get(importPath) || importPath;
+ }
+ const file = fileDir + path.sep + ip.slice(lastSlash + 1);
+ setCoverageDataByFilePath(file, cd);
});
}
@@ -279,7 +266,7 @@
* @param filePath
* @param data
*/
-function setCoverageData(filePath: string, data: CoverageData) {
+function setCoverageDataByFilePath(filePath: string, data: CoverageData) {
if (filePath.startsWith('_')) {
filePath = filePath.substr(1);
}
@@ -289,7 +276,7 @@
filePath = parts.join(path.sep);
}
}
- coverageFiles[filePath] = data;
+ coverageData[filePath] = data;
}
/**
@@ -303,16 +290,16 @@
const cfg = getGoConfig(editor.document.uri);
const coverageOptions = cfg['coverageOptions'];
- for (const filename in coverageFiles) {
+ for (const filename in coverageData) {
if (editor.document.uri.fsPath.endsWith(filename)) {
isCoverageApplied = true;
- const coverageData = coverageFiles[filename];
+ const cd = coverageData[filename];
if (coverageOptions === 'showCoveredCodeOnly' || coverageOptions === 'showBothCoveredAndUncoveredCode') {
editor.setDecorations(
decorators.type === 'gutter'
? decorators.coveredGutterDecorator
: decorators.coveredHighlightDecorator,
- coverageData.coveredRange
+ cd.coveredRange
);
}
@@ -321,7 +308,7 @@
decorators.type === 'gutter'
? decorators.uncoveredGutterDecorator
: decorators.uncoveredHighlightDecorator,
- coverageData.uncoveredRange
+ cd.uncoveredRange
);
}
}
@@ -371,41 +358,6 @@
}
/**
- * Fill the map of directory paths corresponding to input package paths
- * @param set Set<string> of import package paths
- */
-async function getPathsToDirs(set: Set<string>, res: Map<string, string>, testDir?: string) {
- const goRuntimePath = getBinPath('go');
- if (!goRuntimePath) {
- vscode.window.showErrorMessage(
- `Failed to run, as the "go" binary cannot be found in either GOROOT(${process.env['GOROOT']}) or PATH(${envPath})`
- );
- }
- const args: string[] = ['list', '-f', '{{.ImportPath}}:{{.Dir}}'];
- set.forEach((s) => args.push(s));
-
- const options: { [key: string]: any } = {
- env: Object.assign({}, process.env, { GOPATH: getCurrentGoPath() })
- };
- const workDir = getWorkspaceFolderPath();
- // If there is a workDir then probably it is what we want
- // Otherwise maybe a test suggested a directory.
- if (workDir) {
- options['cwd'] = workDir;
- } else if (testDir) {
- options['cwd'] = testDir;
- }
-
- const execFile = util.promisify(cp.execFile);
- const { stdout } = await execFile(goRuntimePath, args, options);
- stdout.split('\n').forEach((line) => {
- const flds = line.split(':');
- if (flds.length !== 2) { return; }
- res.set(flds[0], flds[1]);
- });
-}
-
-/**
* If current editor has Code coverage applied, then remove it.
* Else run tests to get the coverage and apply.
*/
@@ -459,7 +411,7 @@
// These routines enable testing without starting an editing session.
export function coverageFilesForTest(): { [key: string]: CoverageData; } {
- return coverageFiles;
+ return coverageData;
}
export function initForTest() {
diff --git a/src/goPackages.ts b/src/goPackages.ts
index f072ef1..20eefef 100644
--- a/src/goPackages.ts
+++ b/src/goPackages.ts
@@ -260,6 +260,12 @@
*/
export function getNonVendorPackages(
currentFolderPath: string, recursive: boolean = true): Promise<Map<string, string>> {
+
+ const target = recursive ? './...' : '.'; // go list ./... excludes vendor dirs since 1.9
+ return getImportPathToFolder([target], currentFolderPath);
+}
+
+export function getImportPathToFolder(targets: string[], cwd?: string): Promise<Map<string, string>> {
const goRuntimePath = getBinPath('go');
if (!goRuntimePath) {
console.warn(
@@ -267,12 +273,12 @@
);
return;
}
+
return new Promise<Map<string, string>>((resolve, reject) => {
- const target = recursive ? './...' : '.';
const childProcess = cp.spawn(
goRuntimePath,
- ['list', '-f', 'ImportPath: {{.ImportPath}} FolderPath: {{.Dir}}', target],
- { cwd: currentFolderPath, env: toolExecutionEnvironment() }
+ ['list', '-f', 'ImportPath: {{.ImportPath}} FolderPath: {{.Dir}}', ...targets],
+ { cwd, env: toolExecutionEnvironment() }
);
const chunks: any[] = [];
childProcess.stdout.on('data', (stdout) => {
@@ -283,16 +289,13 @@
const lines = chunks.join('').toString().split('\n');
const result = new Map<string, string>();
- const version = await getGoVersion();
- const vendorAlreadyExcluded = version.gt('1.8');
-
lines.forEach((line) => {
const matches = line.match(pkgToFolderMappingRegex);
if (!matches || matches.length !== 3) {
return;
}
const [_, pkgPath, folderPath] = matches;
- if (!pkgPath || (!vendorAlreadyExcluded && pkgPath.includes('/vendor/'))) {
+ if (!pkgPath) {
return;
}
result.set(pkgPath, folderPath);
diff --git a/src/testUtils.ts b/src/testUtils.ts
index b87ef32..aafe427 100644
--- a/src/testUtils.ts
+++ b/src/testUtils.ts
@@ -416,7 +416,7 @@
);
});
if (testconfig.applyCodeCoverage) {
- await applyCodeCoverageToAllEditors(tmpCoverPath);
+ await applyCodeCoverageToAllEditors(tmpCoverPath, testconfig.dir);
}
return testResult;
}
diff --git a/test/integration/coverage.test.ts b/test/integration/coverage.test.ts
index 10e37d2..fe38424 100644
--- a/test/integration/coverage.test.ts
+++ b/test/integration/coverage.test.ts
@@ -36,14 +36,9 @@
initForTest();
const x = vscode.workspace.openTextDocument(coverFilePath);
await applyCodeCoverageToAllEditors(coverFilePath, fixtureSourcePath);
- let aDotGo: boolean;
- let bDotGo: boolean;
- for (const fn in coverageFilesForTest()) {
- if (true) { // TSLint insists the body for for..in.. be an if-statement
- if (fn === `${fixtureSourcePath}/a/a.go`) { aDotGo = true; }
- if (fn === `${fixtureSourcePath}/b/b.go`) { bDotGo = true; }
- }
- }
- assert.equal(aDotGo && bDotGo, true);
+ const files = Object.keys(coverageFilesForTest());
+ const aDotGo = files.includes(path.join(fixtureSourcePath, 'a', 'a.go'));
+ const bDotGo = files.includes(path.join(fixtureSourcePath, 'b', 'b.go'));
+ assert.equal(aDotGo && bDotGo, true, `seen a.go:${aDotGo}, seen b.go:${bDotGo}\n${files}\n`);
});
});