Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Commit

Permalink
Adds support for running gocheck tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tgturner committed May 6, 2020
1 parent e8e97e5 commit 552fb20
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 64 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1573,4 +1573,4 @@
]
}
}
}
}
11 changes: 8 additions & 3 deletions src/goRunTestCodelens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import vscode = require('vscode');
import { CancellationToken, CodeLens, Command, TextDocument } from 'vscode';
import { GoBaseCodeLensProvider } from './goBaseCodelens';
import { GoDocumentSymbolProvider } from './goOutline';
import { getBenchmarkFunctions, getTestFunctions } from './testUtils';
import { getBenchmarkFunctions, getDocumentSymbols, getTestFunctions, hasMethodTests } from './testUtils';
import { getCurrentGoPath, getGoConfig } from './util';

export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider {
Expand Down Expand Up @@ -94,7 +94,10 @@ export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider {
): Promise<CodeLens[]> {
const codelens: CodeLens[] = [];

const testPromise = getTestFunctions(document, token).then((testFunctions) => {
const testPromise = getDocumentSymbols(document, token, true).then((symbols) => {
const [methodTests, _] = hasMethodTests(symbols);
const testFunctions = getTestFunctions(symbols, methodTests);

testFunctions.forEach((func) => {
const runTestCmd: Command = {
title: 'run test',
Expand All @@ -114,7 +117,9 @@ export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider {
});
});

const benchmarkPromise = getBenchmarkFunctions(document, token).then((benchmarkFunctions) => {
const benchmarkPromise = getDocumentSymbols(document, token).then((symbols) => {
const benchmarkFunctions = getBenchmarkFunctions(symbols);

benchmarkFunctions.forEach((func) => {
const runBenchmarkCmd: Command = {
title: 'run benchmark',
Expand Down
65 changes: 38 additions & 27 deletions src/goTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import {
extractInstanceTestName,
findAllTestSuiteRuns,
getBenchmarkFunctions,
getDocumentSymbols,
getTestFlags,
getTestFunctionDebugArgs,
getTestFunctions,
getTestTags,
goTest,
hasMethodTests,
TestConfig
} from './testUtils';
import { getTempFilePath } from './util';
Expand Down Expand Up @@ -45,28 +47,31 @@ export function testAtCursor(goConfig: vscode.WorkspaceConfiguration, cmd: TestA
return;
}

const includeImports = cmd === 'benchmark' ? null : true;
const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;

editor.document.save().then(async () => {
try {
const testFunctions = await getFunctions(editor.document, null);
const documentSymbols = await getDocumentSymbols(editor.document, null, includeImports);
const [methodTests, usingGoCheck] = hasMethodTests(documentSymbols);
const testFunctions = getFunctions(documentSymbols, methodTests);
// We use functionName if it was provided as argument
// Otherwise find any test function containing the cursor.
const testFunctionName =
args && args.functionName
? args.functionName
: testFunctions
.filter((func) => func.range.contains(editor.selection.start))
.map((el) => el.name)[0];
.filter((func) => func.range.contains(editor.selection.start))
.map((el) => el.name)[0];
if (!testFunctionName) {
vscode.window.showInformationMessage('No test function found at cursor.');
return;
}

if (cmd === 'debug') {
await debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
await debugTestAtCursor(editor, testFunctionName, testFunctions, usingGoCheck, goConfig);
} else if (cmd === 'benchmark' || cmd === 'test') {
await runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
await runTestAtCursor(editor, testFunctionName, testFunctions, usingGoCheck, goConfig, cmd, args);
} else {
throw new Error('Unsupported command.');
}
Expand All @@ -83,6 +88,7 @@ async function runTestAtCursor(
editor: vscode.TextEditor,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[],
usingGoCheck: boolean,
goConfig: vscode.WorkspaceConfiguration,
cmd: TestAtCursorCmd,
args: any
Expand All @@ -100,7 +106,8 @@ async function runTestAtCursor(
functions: testConfigFns,
isBenchmark: cmd === 'benchmark',
isMod,
applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTest')
applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTest'),
usingGoCheck
};
// Remember this config as the last executed test.
lastTestConfig = testConfig;
Expand All @@ -114,9 +121,10 @@ async function debugTestAtCursor(
editor: vscode.TextEditor,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[],
usingGoCheck: boolean,
goConfig: vscode.WorkspaceConfiguration
) {
const args = getTestFunctionDebugArgs(editor.document, testFunctionName, testFunctions);
const args = getTestFunctionDebugArgs(editor.document, testFunctionName, testFunctions, usingGoCheck);
const tags = getTestTags(goConfig);
const buildFlags = tags ? ['-tags', tags] : [];
const flagsFromConfig = getTestFlags(goConfig);
Expand Down Expand Up @@ -229,31 +237,34 @@ export async function testCurrentFile(
return;
}

const includeImports = isBenchmark ? null : true;
const getFunctions = isBenchmark ? getBenchmarkFunctions : getTestFunctions;
const isMod = await isModSupported(editor.document.uri);

return editor.document
.save()
.then(() => {
return getFunctions(editor.document, null).then((testFunctions) => {
const testConfig: TestConfig = {
goConfig,
dir: path.dirname(editor.document.fileName),
flags: getTestFlags(goConfig, args),
functions: testFunctions.map((sym) => sym.name),
isBenchmark,
isMod,
applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTestFile')
};
// Remember this config as the last executed test.
lastTestConfig = testConfig;
return goTest(testConfig);
});
})
.then(null, (err) => {
return editor.document.save().then(async () => {
try {
const documentSymbols = await getDocumentSymbols(editor.document, null, includeImports);
const [methodTests, usingGoCheck] = hasMethodTests(documentSymbols);
const testFunctions = getFunctions(documentSymbols, methodTests);

const testConfig: TestConfig = {
goConfig,
dir: path.dirname(editor.document.fileName),
flags: getTestFlags(goConfig, args),
functions: testFunctions.map((sym) => sym.name),
isBenchmark,
isMod,
applyCodeCoverage: goConfig.get<boolean>('coverOnSingleTestFile'),
usingGoCheck
};
// Remember this config as the last executed test.
lastTestConfig = testConfig;
return goTest(testConfig);
} catch (err) {
console.error(err);
return Promise.resolve(false);
});
}
});
}

/**
Expand Down
142 changes: 109 additions & 33 deletions src/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const testFuncRegex = /^Test.*|^Example.*/;
const testMethodRegex = /^\(([^)]+)\)\.(Test.*)$/;
const benchmarkRegex = /^Benchmark.*/;

const goCheckPkg = '"gopkg.in/check.v1"';
const testifyPkg = '"github.com/stretchr/testify/suite"';

/**
* Input to goTest.
*/
Expand Down Expand Up @@ -77,6 +80,10 @@ export interface TestConfig {
* Whether code coverage should be generated and applied.
*/
applyCodeCoverage?: boolean;
/**
* Whether `gocheck` was found in current test file.
*/
usingGoCheck?: boolean;
}

export function getTestEnvVars(config: vscode.WorkspaceConfiguration): any {
Expand Down Expand Up @@ -119,27 +126,55 @@ export function getTestTags(goConfig: vscode.WorkspaceConfiguration): string {
/**
* Returns all Go unit test functions in the given source file.
*
* @param the URI of a Go source file.
* @param symbols symbols in the Go source file.
* @param methodTests whether or not the file contains method tests.
* @return test function symbols for the source file.
*/
export function getTestFunctions(
symbols: vscode.DocumentSymbol[],
methodTests: boolean
): vscode.DocumentSymbol[] {
return symbols.filter(
(sym) =>
sym.kind === vscode.SymbolKind.Function &&
(testFuncRegex.test(sym.name) || (methodTests && testMethodRegex.test(sym.name)))
);
}

/**
* Pull all symbols from the document.
*/
export function getDocumentSymbols(
doc: vscode.TextDocument,
token: vscode.CancellationToken
token: vscode.CancellationToken,
includeImports?: boolean
): Thenable<vscode.DocumentSymbol[]> {
const documentSymbolProvider = new GoDocumentSymbolProvider(true);
const documentSymbolProvider = new GoDocumentSymbolProvider(includeImports);
return documentSymbolProvider
.provideDocumentSymbols(doc, token)
.then((symbols) => symbols[0].children)
.then((symbols) => {
const testify = symbols.some(
(sym) => sym.kind === vscode.SymbolKind.Namespace && sym.name === '"github.com/stretchr/testify/suite"'
);
return symbols.filter(
(sym) =>
sym.kind === vscode.SymbolKind.Function &&
(testFuncRegex.test(sym.name) || (testify && testMethodRegex.test(sym.name)))
);
});
.then((symbols) => symbols[0].children);
}

/**
* Check symbols for the presence of test suites that use methods.
*/
export function hasMethodTests(
symbols: vscode.DocumentSymbol[]
): boolean[] {
let usingGoChck = false;
const usingMethodTests = symbols.some(
(sym) => {
const isNamespace = sym.kind === vscode.SymbolKind.Namespace;
if (isNamespace && sym.name === goCheckPkg) {
usingGoChck = true;
return true;
} else {
return isNamespace && sym.name === testifyPkg;
}
}
);

return [usingMethodTests, usingGoChck];
}

/**
Expand All @@ -156,20 +191,46 @@ export function extractInstanceTestName(symbolName: string): string {
return match[2];
}

/**
* Extracts test method name and the method suite of a suite test function.
* For example a symbol with name "(*testSuite).TestMethod" will return ["testSuite", "TestMethod"].
*
* @param symbolName Symbol Name to extract method name from.
*/
export function extractInstanceTestNameWithSuite(symbolName: string): string[] {
const match = symbolName.match(testMethodRegex);
if (!match || match.length !== 3) {
return null;
}
const [suiteName, methodName] = match.slice(1, 3);
let trimmedSuiteName = suiteName;
if (trimmedSuiteName[0] === '*') {
trimmedSuiteName = trimmedSuiteName.substring(1);
}

return [trimmedSuiteName, methodName];
}

/**
* Gets the appropriate debug arguments for a debug session on a test function.
* @param document The document containing the tests
* @param testFunctionName The test function to get the debug args
* @param testFunctions The test functions found in the document
* @param usingGoCheck Whether or not the file is using `gocheck`
*/
export function getTestFunctionDebugArgs(
document: vscode.TextDocument,
testFunctionName: string,
testFunctions: vscode.DocumentSymbol[]
testFunctions: vscode.DocumentSymbol[],
usingGoCheck: boolean,
): string[] {
if (benchmarkRegex.test(testFunctionName)) {
return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
}
if (usingGoCheck) {
const [suiteName, testName] = extractInstanceTestNameWithSuite(testFunctionName);
return ['-check.f', `${suiteName}.${testName}`];
}
const instanceMethod = extractInstanceTestName(testFunctionName);
if (instanceMethod) {
const testFns = findAllTestSuiteRuns(document, testFunctions);
Expand Down Expand Up @@ -199,26 +260,20 @@ export function findAllTestSuiteRuns(
/**
* Returns all Benchmark functions in the given source file.
*
* @param the URI of a Go source file.
* @param symbols symbols in the Go source file.
* @return benchmark function symbols for the source file.
*/
export function getBenchmarkFunctions(
doc: vscode.TextDocument,
token: vscode.CancellationToken
): Thenable<vscode.DocumentSymbol[]> {
const documentSymbolProvider = new GoDocumentSymbolProvider();
return documentSymbolProvider
.provideDocumentSymbols(doc, token)
.then((symbols) => symbols[0].children)
.then((symbols) =>
symbols.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name))
);
symbols: vscode.DocumentSymbol[],
_?: boolean
): vscode.DocumentSymbol[] {
return symbols.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name));
}

/**
* Runs go test and presents the output in the 'Go' channel.
*
* @param goConfig Configuration for the Go extension.
* @param testconfig Configuration for running tests.
*/
export async function goTest(testconfig: TestConfig): Promise<boolean> {
const tmpCoverPath = getTempFilePath('go-code-cover');
Expand Down Expand Up @@ -444,11 +499,12 @@ function targetArgs(testconfig: TestConfig): Array<string> {
params = ['-bench', util.format('^(%s)$', testconfig.functions.join('|'))];
} else {
let testFunctions = testconfig.functions;
let testifyMethods = testFunctions.filter((fn) => testMethodRegex.test(fn));
if (testifyMethods.length > 0) {
// filter out testify methods
const testMethods = testFunctions.filter((fn) => testMethodRegex.test(fn));
let testMethodsWithSuite: string[][] = [];
if (testMethods.length > 0) {
// filter out test methods
testFunctions = testFunctions.filter((fn) => !testMethodRegex.test(fn));
testifyMethods = testifyMethods.map(extractInstanceTestName);
testMethodsWithSuite = testMethods.map(extractInstanceTestNameWithSuite);
}

// we might skip the '-run' param when running only testify methods, which will result
Expand All @@ -457,8 +513,28 @@ function targetArgs(testconfig: TestConfig): Array<string> {
if (testFunctions.length > 0) {
params = params.concat(['-run', util.format('^(%s)$', testFunctions.join('|'))]);
}
if (testifyMethods.length > 0) {
params = params.concat(['-testify.m', util.format('^(%s)$', testifyMethods.join('|'))]);

const numMethods = testMethodsWithSuite.length;
if (numMethods > 0) {
if (testconfig.usingGoCheck) {
const testNames = testMethodsWithSuite.reduce((acc, [suiteName, instanceMethod], index) => {
if (index === numMethods - 1) {
return acc + `${suiteName}.${instanceMethod}`;
} else {
return acc + `${suiteName}.${instanceMethod}|`;
}
}, '');
params = params.concat(['-check.f', util.format('(%s)', testNames)]);
} else {
const testNames = testMethodsWithSuite.reduce((acc, [_, instanceMethod], index) => {
if (index === numMethods - 1) {
return acc + instanceMethod;
} else {
return acc + `${instanceMethod}|`;
}
}, '');
params = params.concat(['-testify.m', util.format('^(%s)$', testNames)]);
}
}
}
return params;
Expand Down

0 comments on commit 552fb20

Please sign in to comment.