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

Added support for tests using stretchr/testify #1707

Merged
merged 6 commits into from
Jun 20, 2018
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/goRunTestCodelens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import vscode = require('vscode');
import path = require('path');
import { TextDocument, CancellationToken, CodeLens, Command } from 'vscode';
import { getTestFunctions, getBenchmarkFunctions, getTestFlags } from './testUtils';
import { getTestFunctions, getBenchmarkFunctions, getTestFlags, extractInstanceTestName, findTestFnForInstanceTest } from './testUtils';
import { GoDocumentSymbolProvider } from './goOutline';
import { getCurrentGoPath } from './util';
import { GoBaseCodeLensProvider } from './goBaseCodelens';
Expand Down Expand Up @@ -94,10 +94,22 @@ export class GoRunTestCodeLensProvider extends GoBaseCodeLensProvider {

codelens.push(new CodeLens(func.location.range, runTestCmd));

let args: string[] = [];
let instanceMethod = extractInstanceTestName(func.name);
if (instanceMethod) {
const testFn = findTestFnForInstanceTest(func.name, document, testFunctions);
if (testFn) {
args = args.concat('-test.run', `^${testFn.name}$`);
}
args = args.concat('-testify.m', `^${instanceMethod}$`);
} else {
args = args.concat('-test.run', `^${func.name}$`);
}

let debugTestCmd: Command = {
title: 'debug test',
command: 'go.debug.startSession',
arguments: [Object.assign({}, currentDebugConfig, { args: ['-test.run', '^' + func.name + '$'] })]
arguments: [Object.assign({}, currentDebugConfig, { args: args })]
};

codelens.push(new CodeLens(func.location.range, debugTestCmd));
Expand Down
30 changes: 19 additions & 11 deletions src/goTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import path = require('path');
import vscode = require('vscode');
import os = require('os');
import { goTest, TestConfig, getTestFlags, getTestFunctions, getBenchmarkFunctions } from './testUtils';
import { goTest, TestConfig, getTestFlags, getTestFunctions, getBenchmarkFunctions, extractInstanceTestName, findTestFnForInstanceTest } from './testUtils';
import { getCoverage } from './goCover';

// lastTestConfig holds a reference to the last executed TestConfig which allows
Expand All @@ -34,7 +34,7 @@ export function testAtCursor(goConfig: vscode.WorkspaceConfiguration, isBenchmar

const getFunctions = isBenchmark ? getBenchmarkFunctions : getTestFunctions;

const {tmpCoverPath, testFlags } = makeCoverData(goConfig, 'coverOnSingleTest', args);
const { tmpCoverPath, testFlags } = makeCoverData(goConfig, 'coverOnSingleTest', args);

editor.document.save().then(() => {
return getFunctions(editor.document, null).then(testFunctions => {
Expand All @@ -59,13 +59,22 @@ export function testAtCursor(goConfig: vscode.WorkspaceConfiguration, isBenchmar
return;
}

const testConfig = {
const testConfigFns = [testFunctionName];

if (!isBenchmark && extractInstanceTestName(testFunctionName)) {
// find test function with corresponding suite.Run
const t = findTestFnForInstanceTest(testFunctionName, editor.document, testFunctions);
if (t) {
testConfigFns.push(t.name);
}
}

const testConfig: TestConfig = {
goConfig: goConfig,
dir: path.dirname(editor.document.fileName),
flags: testFlags,
functions: [testFunctionName],
functions: testConfigFns,
isBenchmark: isBenchmark,
showTestCoverage: true
};

// Remember this config as the last executed test.
Expand Down Expand Up @@ -94,13 +103,12 @@ export function testCurrentPackage(goConfig: vscode.WorkspaceConfiguration, args
return;
}

const {tmpCoverPath, testFlags } = makeCoverData(goConfig, 'coverOnTestPackage', args);
const { tmpCoverPath, testFlags } = makeCoverData(goConfig, 'coverOnTestPackage', args);

const testConfig = {
const testConfig: TestConfig = {
goConfig: goConfig,
dir: path.dirname(editor.document.fileName),
flags: testFlags,
showTestCoverage: true
};
// Remember this config as the last executed test.
lastTestConfig = testConfig;
Expand Down Expand Up @@ -160,11 +168,11 @@ export function testCurrentFile(goConfig: vscode.WorkspaceConfiguration, args: s

return editor.document.save().then(() => {
return getTestFunctions(editor.document, null).then(testFunctions => {
const testConfig = {
const testConfig: TestConfig = {
goConfig: goConfig,
dir: path.dirname(editor.document.fileName),
flags: getTestFlags(goConfig, args),
functions: testFunctions.map(func => { return func.name; })
functions: testFunctions.map(sym => sym.name),
};
// Remember this config as the last executed test.
lastTestConfig = testConfig;
Expand Down Expand Up @@ -203,5 +211,5 @@ function makeCoverData(goConfig: vscode.WorkspaceConfiguration, confFlag: string
testFlags.push('-coverprofile=' + tmpCoverPath);
}

return {tmpCoverPath, testFlags};
return { tmpCoverPath, testFlags };
}
83 changes: 80 additions & 3 deletions src/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getNonVendorPackages } from './goPackages';

let outputChannel = vscode.window.createOutputChannel('Go Tests');

const testSuiteMethodRegex = /^\(([^)]+)\)\.(Test.*)$/;

/**
* Input to goTest.
Expand Down Expand Up @@ -89,10 +90,64 @@ export function getTestFunctions(doc: vscode.TextDocument, token: vscode.Cancell
.then(symbols =>
symbols.filter(sym =>
sym.kind === vscode.SymbolKind.Function
&& (sym.name.startsWith('Test') || sym.name.startsWith('Example')))
&& (sym.name.startsWith('Test') || sym.name.startsWith('Example') || testSuiteMethodRegex.test(sym.name))
)
);
}

/**
* Extracts test method name of a suite test function.
* For example a symbol with name "(*testSuite).TestMethod" will return "TestMethod".
*
* @param symbolName Symbol Name to extract method name from.
*/
export function extractInstanceTestName(symbolName: string): string {
const match = symbolName.match(testSuiteMethodRegex);
if (!match || match.length !== 3) {
return null;
}
return match[2];
}

/**
* Finds test method containing suite.Run() call for the given instance test name.
*
* @param symbolName Instance test function name (including the type name)
* @param doc Editor document
* @param allTests All test functions
*/
export function findTestFnForInstanceTest(symbolName: string, doc: vscode.TextDocument, allTests: vscode.SymbolInformation[]): vscode.SymbolInformation {
// get non-instance test functions
const testFunctions = allTests.filter(t => !testSuiteMethodRegex.test(t.name));
// filter further to ones containing suite.Run()
const candidates = testFunctions.filter(t => {
const text = doc.getText(t.location.range);
return /suite\.Run\(/.test(text);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a simple string check, wont text.includes('suite.Run(') be simpler?
Unless we make this a useful regex like `/suite.Run([^\)]+)/

});

switch (candidates.length) {
case 0: return null;
case 1: return candidates[0];
}

// we have multiple matches, filter even more
const match = symbolName.match(testSuiteMethodRegex);
if (!match || match.length !== 3) {
return candidates[0];
}
// drop pointers, lowercase for more lenient matching
const instanceType = match[1].replace(/[*]/g, '').toLowerCase();
const filtered = candidates.filter(t => {
const text = doc.getText(t.location.range).toLowerCase();
if (text.includes(instanceType)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may give false positives if say there is a comment inside the function with another suite's name.
It can give false negatives if the suite was instantiated outside the function.

Why not return all the functions that have a suite.Run()?

Since we also pass -testify.m, the other suites will be a no-op right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed, clearly this isn't going to work in 100% of cases, but should be close enough.

return true;
}
// try the test name as well
return t.name.substring(4).includes(instanceType);
});
return filtered.length > 0 ? filtered[0] : candidates[0];
}

/**
* Returns all Benchmark functions in the given source file.
*
Expand Down Expand Up @@ -152,7 +207,7 @@ export function goTest(testconfig: TestConfig): Thenable<boolean> {

targetArgs(testconfig).then(targets => {
let outTargets = args.slice(0);
if (targets.length > 2) {
if (targets.length > 4) {
outTargets.push('<long arguments omitted>');
} else {
outTargets.push(...targets);
Expand Down Expand Up @@ -242,7 +297,29 @@ function expandFilePathInOutput(output: string, cwd: string): string {
*/
function targetArgs(testconfig: TestConfig): Thenable<Array<string>> {
if (testconfig.functions) {
return Promise.resolve([testconfig.isBenchmark ? '-bench' : '-run', util.format('^%s$', testconfig.functions.join('|'))]);
let params: string[] = [];
if (testconfig.isBenchmark) {
params = ['-bench', util.format('^%s$', testconfig.functions.join('|'))];
} else {
let testFunctions = testconfig.functions;
let testifyMethods = testFunctions.filter(fn => testSuiteMethodRegex.test(fn));
if (testifyMethods.length > 0) {
// filter out testify methods
testFunctions = testFunctions.filter(fn => !testSuiteMethodRegex.test(fn));
testifyMethods = testifyMethods.map(extractInstanceTestName);
}

// we'll skip the '-run' param when running only testify methods, which will result
// in running all the test methods, but that is necessary, cause one of them should
// call testify's `suite.Run(...)`
if (testFunctions.length > 0) {
params = params.concat(['-run', util.format('^%s$', testFunctions.join('|'))]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to pass -run ^$ even if test.Functions array is empty. Otherwise all tests will be run

}
if (testifyMethods.length > 0) {
params = params.concat(['-testify.m', util.format('^%s$', testifyMethods.join('|'))]);
}
}
return Promise.resolve(params);
} else if (testconfig.includeSubDirectories && !testconfig.isBenchmark) {
return getGoVersion().then((ver: SemVersion) => {
if (ver && (ver.major > 1 || (ver.major === 1 && ver.minor >= 9))) {
Expand Down