Skip to content

Commit

Permalink
Add go.testOnSave option (microsoft#810)
Browse files Browse the repository at this point in the history
Signed-off-by: Oleg Bulatov <oleg@bulatov.me>
  • Loading branch information
dmage authored and ramya-rao-a committed Feb 26, 2017
1 parent 2c88e72 commit e40b400
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 78 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following Visual Studio Code settings are available for the Go extension. T
"go.lintTool": "golint",
"go.lintFlags": [],
"go.vetFlags": [],
"go.testOnSave": false,
"go.coverOnSave": false,
"go.useCodeSnippetsOnFunctionSuggest": false,
"go.formatOnSave": true,
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@
"default": null,
"description": "Specifies the GOROOT to use when no environment variable is set."
},
"go.testOnSave": {
"type": "boolean",
"default": false,
"description": "Run 'go test' on save."
},
"go.coverOnSave": {
"type": "boolean",
"default": false,
Expand Down
86 changes: 84 additions & 2 deletions src/goCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,43 @@ import { getGoRuntimePath } from './goPath';
import { getCoverage } from './goCover';
import { outputChannel } from './goStatus';
import { promptForMissingTool } from './goInstallTools';
import { goTest } from './goTest';
import { getBinPath, parseFilePrelude } from './util';

let statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
statusBarItem.command = 'go.test.showOutput';

export function removeTestStatus(e: vscode.TextDocumentChangeEvent) {
if (e.document.isUntitled || e.document.languageId !== 'go') {
return;
}
statusBarItem.hide();
statusBarItem.text = '';
}

export function showHideTestStatus() {
if (!vscode.window.activeTextEditor) {
statusBarItem.hide();
return;
}

let goConfig = vscode.workspace.getConfiguration('go');
if (!goConfig['testOnSave']) {
statusBarItem.hide();
return;
}

let editor = vscode.window.activeTextEditor;
if (editor.document.languageId !== 'go') {
statusBarItem.hide();
return;
}

if (statusBarItem.text !== '') {
statusBarItem.show();
}
}

export interface ICheckResult {
file: string;
line: number;
Expand All @@ -27,7 +62,7 @@ export interface ICheckResult {
* Runs given Go tool and returns errors/warnings that can be fed to the Problems Matcher
* @param args Arguments to be passed while running given tool
* @param cwd cwd that will passed in the env object while running given tool
* @param serverity error or warning
* @param severity error or warning
* @param useStdErr If true, the stderr of the output of the given tool will be used, else stdout will be used
* @param toolName The name of the Go tool to run. If none is provided, the go runtime itself is used
* @param printUnexpectedOutput If true, then output that doesnt match expected format is printed to the output channel
Expand Down Expand Up @@ -103,6 +138,30 @@ export function check(filename: string, goConfig: vscode.WorkspaceConfiguration)
return Promise.resolve([]);
}

let testPromise: Thenable<boolean>;
let tmpCoverPath;
let runTest = () => {
if (testPromise) {
return testPromise;
}

let buildFlags = goConfig['testFlags'] || goConfig['buildFlags'] || [];

let args = buildFlags;
if (goConfig['coverOnSave']) {
tmpCoverPath = path.normalize(path.join(os.tmpdir(), 'go-code-cover'));
args = ['-coverprofile=' + tmpCoverPath, ...buildFlags];
}

testPromise = goTest({
goConfig: goConfig,
dir: cwd,
flags: args,
background: true
});
return testPromise;
};

if (!!goConfig['buildOnSave']) {
// we need to parse the file to check the package name
// if the package is a main pkg, we won't be doing a go build -i
Expand Down Expand Up @@ -140,6 +199,22 @@ export function check(filename: string, goConfig: vscode.WorkspaceConfiguration)
});
runningToolsPromises.push(buildPromise);
}

if (!!goConfig['testOnSave']) {
statusBarItem.show();
statusBarItem.text = 'testing';
runTest().then(success => {
if (statusBarItem.text === '') {
return;
}
if (success) {
statusBarItem.text = 'OK :-)';
} else {
statusBarItem.text = 'FAIL :-(';
}
});
}

if (!!goConfig['lintOnSave']) {
let lintTool = goConfig['lintTool'] || 'golint';
let lintFlags: string[] = goConfig['lintFlags'] || [];
Expand Down Expand Up @@ -171,7 +246,14 @@ export function check(filename: string, goConfig: vscode.WorkspaceConfiguration)
}

if (!!goConfig['coverOnSave']) {
runningToolsPromises.push(getCoverage(filename));
let coverPromise = runTest().then(success => {
if (!success) {
return [];
}
// FIXME: it's not obvious that tmpCoverPath comes from runTest()
return getCoverage(tmpCoverPath);
});
runningToolsPromises.push(coverPromise);
}

return Promise.all(runningToolsPromises).then(resultSets => [].concat.apply([], resultSets));
Expand Down
135 changes: 66 additions & 69 deletions src/goCover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path = require('path');
import os = require('os');
import fs = require('fs');
import { getGoRuntimePath } from './goPath';
import { showTestOutput, goTest } from './goTest';
import { getBinPath } from './util';
import rl = require('readline');
import { outputChannel } from './goStatus';
Expand Down Expand Up @@ -58,7 +59,26 @@ export function coverageCurrentPackage() {
vscode.window.showInformationMessage('No editor is active.');
return;
}
getCoverage(editor.document.uri.fsPath, true);

// FIXME: is there a better way to get goConfig?
let goConfig = vscode.workspace.getConfiguration('go');
let cwd = path.dirname(editor.document.uri.fsPath);

let buildFlags = goConfig['testFlags'] || goConfig['buildFlags'] || [];
let tmpCoverPath = path.normalize(path.join(os.tmpdir(), 'go-code-cover'));
let args = ['-coverprofile=' + tmpCoverPath, ...buildFlags];
return goTest({
goConfig: goConfig,
dir: cwd,
flags: args,
background: true
}).then(success => {
if (!success) {
showTestOutput();
return [];
}
return getCoverage(tmpCoverPath, true);
});
}

export function getCodeCoverage(editor: vscode.TextEditor) {
Expand Down Expand Up @@ -90,75 +110,52 @@ function highlightCoverage(editor: vscode.TextEditor, file: CoverageFile, remove
editor.setDecorations(coveredHighLight, remove ? [] : file.coveredRange);
}

export function getCoverage(filename: string, showErrOutput: boolean = false): Promise<any[]> {
export function getCoverage(coverProfilePath: string, showErrOutput: boolean = false): Promise<any[]> {
return new Promise((resolve, reject) => {
let tmppath = path.normalize(path.join(os.tmpdir(), 'go-code-cover'));
let cwd = path.dirname(filename);
let args = ['test', '-coverprofile=' + tmppath];
let goRuntimePath = getGoRuntimePath();

if (!goRuntimePath) {
vscode.window.showInformationMessage('Cannot find "go" binary. Update PATH or GOROOT appropriately');
return Promise.resolve([]);
}

// make sure tmppath exists
fs.closeSync(fs.openSync(tmppath, 'a'));

cp.execFile(goRuntimePath, args, { cwd: cwd }, (err, stdout, stderr) => {
try {
// Clear existing coverage files
clearCoverage();
if (err && (<any>err).code !== 0) {
outputChannel.appendLine(['Finished running tool:', goRuntimePath, ...args].join(' '));
outputChannel.appendLine(((<any>err).message));
if (showErrOutput) {
outputChannel.show(true);
}
return resolve([]);
try {
// Clear existing coverage files
clearCoverage();

let lines = rl.createInterface({
input: fs.createReadStream(coverProfilePath),
output: undefined
});

lines.on('line', function (data: string) {
// go test coverageprofile generates output:
// filename:StartLine.StartColumn,EndLine.EndColumn Hits IsCovered
// The first line will be "mode: set" which will be ignored
let fileRange = data.match(/([^:]+)\:([\d]+)\.([\d]+)\,([\d]+)\.([\d]+)\s([\d]+)\s([\d]+)/);
if (!fileRange) return;

let coverage = coverageFiles[fileRange[1]] || { coveredRange: [], uncoveredRange: [] };
let range = new vscode.Range(
// Start Line converted to zero based
parseInt(fileRange[2]) - 1,
// Start Column converted to zero based
parseInt(fileRange[3]) - 1,
// End Line converted to zero based
parseInt(fileRange[4]) - 1,
// End Column converted to zero based
parseInt(fileRange[5]) - 1
);
// If is Covered
if (parseInt(fileRange[7]) === 1) {
coverage.coveredRange.push({ range });
}

let lines = rl.createInterface({
input: fs.createReadStream(tmppath),
output: undefined
});

lines.on('line', function (data: string) {
// go test coverageprofile generates output:
// filename:StartLine.StartColumn,EndLine.EndColumn Hits IsCovered
// The first line will be "mode: set" which will be ignored
let fileRange = data.match(/([^:]+)\:([\d]+)\.([\d]+)\,([\d]+)\.([\d]+)\s([\d]+)\s([\d]+)/);
if (!fileRange) return;

let coverage = coverageFiles[fileRange[1]] || { coveredRange: [], uncoveredRange: [] };
let range = new vscode.Range(
// Start Line converted to zero based
parseInt(fileRange[2]) - 1,
// Start Column converted to zero based
parseInt(fileRange[3]) - 1,
// End Line converted to zero based
parseInt(fileRange[4]) - 1,
// End Column converted to zero based
parseInt(fileRange[5]) - 1
);
// If is Covered
if (parseInt(fileRange[7]) === 1) {
coverage.coveredRange.push({ range });
}
// Not Covered
else {
coverage.uncoveredRange.push({ range });
}
coverageFiles[fileRange[1]] = coverage;
});
lines.on('close', function (data) {
applyCoverage();
resolve([]);
});
} catch (e) {
vscode.window.showInformationMessage(e.msg);
reject(e);
}
});
// Not Covered
else {
coverage.uncoveredRange.push({ range });
}
coverageFiles[fileRange[1]] = coverage;
});
lines.on('close', function (data) {
applyCoverage();
resolve([]);
});
} catch (e) {
vscode.window.showInformationMessage(e.msg);
reject(e);
}
});
}
10 changes: 8 additions & 2 deletions src/goMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import { GoDocumentSymbolProvider } from './goOutline';
import { GoSignatureHelpProvider } from './goSignature';
import { GoWorkspaceSymbolProvider } from './goSymbol';
import { GoCodeActionProvider } from './goCodeAction';
import { check, ICheckResult } from './goCheck';
import { check, ICheckResult, removeTestStatus, showHideTestStatus } from './goCheck';
import { updateGoPathGoRootFromConfig, setupGoPathAndOfferToInstallTools } from './goInstallTools';
import { GO_MODE } from './goMode';
import { showHideStatus } from './goStatus';
import { coverageCurrentPackage, getCodeCoverage, removeCodeCoverage } from './goCover';
import { testAtCursor, testCurrentPackage, testCurrentFile, testPrevious } from './goTest';
import { testAtCursor, testCurrentPackage, testCurrentFile, testPrevious, showTestOutput } from './goTest';
import * as goGenerateTests from './goGenerateTests';
import { addImport } from './goImport';
import { installAllTools, checkLanguageServer } from './goInstallTools';
Expand Down Expand Up @@ -74,8 +74,10 @@ export function activate(ctx: vscode.ExtensionContext): void {
diagnosticCollection = vscode.languages.createDiagnosticCollection('go');
ctx.subscriptions.push(diagnosticCollection);
vscode.workspace.onDidChangeTextDocument(removeCodeCoverage, null, ctx.subscriptions);
vscode.workspace.onDidChangeTextDocument(removeTestStatus, null, ctx.subscriptions);
vscode.window.onDidChangeActiveTextEditor(showHideStatus, null, ctx.subscriptions);
vscode.window.onDidChangeActiveTextEditor(getCodeCoverage, null, ctx.subscriptions);
vscode.window.onDidChangeActiveTextEditor(showHideTestStatus, null, ctx.subscriptions);

setupGoPathAndOfferToInstallTools();
startBuildOnSaveWatcher(ctx.subscriptions);
Expand Down Expand Up @@ -115,6 +117,10 @@ export function activate(ctx: vscode.ExtensionContext): void {
coverageCurrentPackage();
}));

ctx.subscriptions.push(vscode.commands.registerCommand('go.test.showOutput', () => {
showTestOutput();
}));

ctx.subscriptions.push(vscode.commands.registerCommand('go.import.add', (arg: string) => {
return addImport(typeof arg === 'string' ? arg : null);
}));
Expand Down
Loading

0 comments on commit e40b400

Please sign in to comment.