src: support running an individual subtest

Similar to 'Test function at cursor', this allows running a test by placing
the cursor inside the test function or on the t.Run call. It will search
back from the current cursor position within the current outer test
function for a line that contains t.Run. If a subtest is found, an
appropriate call to runTestAtCursor is constructed and the test is
executed by existing go test functionality.

This is a port of https://github.com/microsoft/vscode-go/pull/3199

Change-Id: Ibb2d1267bd44aa370e2cf9ff6554c18f0d67815c
GitHub-Last-Rev: b8e33c04f24242727fa9b6fda1a06761cde88207
GitHub-Pull-Request: golang/vscode-go#87
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/235447
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/package.json b/package.json
index 49846aa..8fb9b57 100644
--- a/package.json
+++ b/package.json
@@ -162,6 +162,11 @@
         "description": "Runs a unit test at the cursor."
       },
       {
+        "command": "go.subtest.cursor",
+        "title": "Go: Subtest At Cursor",
+        "description": "Runs a sub test at the cursor."
+      },
+      {
         "command": "go.benchmark.cursor",
         "title": "Go: Benchmark Function At Cursor",
         "description": "Runs a benchmark at the cursor."
diff --git a/src/goMain.ts b/src/goMain.ts
index 3451bd5..732f424 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -38,7 +38,7 @@
 import { GoReferencesCodeLensProvider } from './goReferencesCodelens';
 import { GoRunTestCodeLensProvider } from './goRunTestCodelens';
 import { outputChannel, showHideStatus } from './goStatus';
-import { testAtCursor, testCurrentFile, testCurrentPackage, testPrevious, testWorkspace } from './goTest';
+import { subTestAtCursor, testAtCursor, testCurrentFile, testCurrentPackage, testPrevious, testWorkspace } from './goTest';
 import { getConfiguredTools } from './goTools';
 import { vetCode } from './goVet';
 import {
@@ -274,6 +274,13 @@
 	);
 
 	ctx.subscriptions.push(
+		vscode.commands.registerCommand('go.subtest.cursor', (args) => {
+			const goConfig = getGoConfig();
+			subTestAtCursor(goConfig, args);
+		})
+	);
+
+	ctx.subscriptions.push(
 		vscode.commands.registerCommand('go.debug.cursor', (args) => {
 			const goConfig = getGoConfig();
 			testAtCursor(goConfig, 'debug', args);
diff --git a/src/goTest.ts b/src/goTest.ts
index cc76650..a523a6e 100644
--- a/src/goTest.ts
+++ b/src/goTest.ts
@@ -108,6 +108,66 @@
 }
 
 /**
+ * Executes the sub unit test at the primary cursor using `go test`. Output
+ * is sent to the 'Go' channel.
+ *
+ * @param goConfig Configuration for the Go extension.
+ */
+export async function subTestAtCursor(goConfig: vscode.WorkspaceConfiguration, args: any) {
+	const editor = vscode.window.activeTextEditor;
+	if (!editor) {
+		vscode.window.showInformationMessage('No editor is active.');
+		return;
+	}
+	if (!editor.document.fileName.endsWith('_test.go')) {
+		vscode.window.showInformationMessage('No tests found. Current file is not a test file.');
+		return;
+	}
+
+	await editor.document.save();
+	try {
+		const testFunctions = await getTestFunctions(editor.document, null);
+		// We use functionName if it was provided as argument
+		// Otherwise find any test function containing the cursor.
+		const currentTestFunctions = testFunctions.filter((func) => func.range.contains(editor.selection.start));
+		const testFunctionName =
+			args && args.functionName ? args.functionName : currentTestFunctions.map((el) => el.name)[0];
+
+		if (!testFunctionName || currentTestFunctions.length === 0) {
+			vscode.window.showInformationMessage('No test function found at cursor.');
+			return;
+		}
+
+		const testFunction = currentTestFunctions[0];
+		const simpleRunRegex = /t.Run\("([^"]+)",/;
+		const runRegex = /t.Run\(/;
+		let lineText: string;
+		let runMatch: RegExpMatchArray | null;
+		let simpleMatch: RegExpMatchArray | null;
+		for (let i = editor.selection.start.line; i >= testFunction.range.start.line; i--) {
+			lineText = editor.document.lineAt(i).text;
+			simpleMatch = lineText.match(simpleRunRegex);
+			runMatch = lineText.match(runRegex);
+			if (simpleMatch || (runMatch && !simpleMatch)) {
+				break;
+			}
+		}
+
+		if (!simpleMatch) {
+			vscode.window.showInformationMessage('No subtest function with a simple subtest name found at cursor.');
+			return;
+		}
+
+		const subTestName = testFunctionName + '/' + simpleMatch[1];
+
+		return await runTestAtCursor(editor, subTestName, testFunctions, goConfig, 'test', args);
+	} catch (err) {
+		vscode.window.showInformationMessage('Unable to run subtest: ' + err.toString());
+		console.error(err);
+	}
+}
+
+/**
  * Debugs the test at cursor.
  */
 async function debugTestAtCursor(
diff --git a/src/testUtils.ts b/src/testUtils.ts
index c318b0f..d1a97f4 100644
--- a/src/testUtils.ts
+++ b/src/testUtils.ts
@@ -463,7 +463,11 @@
 			// in running all the test methods, but one of them should call testify's `suite.Run(...)`
 			// which will result in the correct thing to happen
 			if (testFunctions.length > 0) {
-				params = params.concat(['-run', util.format('^(%s)$', testFunctions.join('|'))]);
+				if (testFunctions.length === 1) {
+					params = params.concat(['-run', util.format('^%s$', testFunctions.pop())]);
+				} else {
+					params = params.concat(['-run', util.format('^(%s)$', testFunctions.join('|'))]);
+				}
 			}
 			if (testifyMethods.length > 0) {
 				params = params.concat(['-testify.m', util.format('^(%s)$', testifyMethods.join('|'))]);
diff --git a/test/fixtures/subtests/go.mod b/test/fixtures/subtests/go.mod
new file mode 100644
index 0000000..1455032
--- /dev/null
+++ b/test/fixtures/subtests/go.mod
@@ -0,0 +1,3 @@
+module github.com/microsoft/vscode-go/gofixtures/subtests
+
+go 1.14
diff --git a/test/fixtures/subtests/subtests_test.go b/test/fixtures/subtests/subtests_test.go
new file mode 100644
index 0000000..3481139
--- /dev/null
+++ b/test/fixtures/subtests/subtests_test.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"testing"
+)
+
+func TestSample(t *testing.T) {
+	t.Run("sample test passing", func(t *testing.T) {
+
+	})
+
+	t.Run("sample test failing", func(t *testing.T) {
+		t.FailNow()
+	})
+
+	testName := "dynamic test name"
+	t.Run(testName, func(t *testing.T) {
+		t.FailNow()
+	})
+}
diff --git a/test/integration/extension.test.ts b/test/integration/extension.test.ts
index f0ed8a7..1299a92 100644
--- a/test/integration/extension.test.ts
+++ b/test/integration/extension.test.ts
@@ -28,7 +28,7 @@
 import { GoSignatureHelpProvider } from '../../src/goSignature';
 import { GoCompletionItemProvider } from '../../src/goSuggest';
 import { getWorkspaceSymbols } from '../../src/goSymbol';
-import { testCurrentFile } from '../../src/goTest';
+import { subTestAtCursor, testCurrentFile } from '../../src/goTest';
 import {
 	getBinPath,
 	getCurrentGoPath,
@@ -113,6 +113,10 @@
 			path.join(fixtureSourcePath, 'diffTestData', 'file2.go'),
 			path.join(fixturePath, 'diffTest2Data', 'file2.go')
 		);
+		fs.copySync(
+			path.join(fixtureSourcePath, 'subtests', 'subtests_test.go'),
+			path.join(fixturePath, 'subtests', 'subtests_test.go')
+		);
 	});
 
 	suiteTeardown(() => {
@@ -1538,4 +1542,76 @@
 		await runFillStruct(editor);
 		assert.equal(vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.getText(), golden);
 	});
+
+	test('Subtests - runs a test with cursor on t.Run line', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(7, 4, 7, 4);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, []);
+		assert.equal(result, true);
+	});
+
+	test('Subtests - runs a test with cursor within t.Run function', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(8, 4, 8, 4);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, []);
+		assert.equal(result, true);
+	});
+
+	test('Subtests - returns false for a failing test', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(11, 4, 11, 4);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, []);
+		assert.equal(result, false);
+	});
+
+	test('Subtests - does nothing for a dynamically defined subtest', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(17, 4, 17, 4);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, []);
+		assert.equal(result, undefined);
+	});
+
+	test('Subtests - does nothing when cursor outside of a test function', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(5, 0, 5, 0);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, []);
+		assert.equal(result, undefined);
+	});
+
+	test('Subtests - does nothing when no test function covers the cursor and a function name is passed in', async () => {
+		const config = vscode.workspace.getConfiguration('go');
+		const uri = vscode.Uri.file(path.join(fixturePath, 'subtests', 'subtests_test.go'));
+		const document = await vscode.workspace.openTextDocument(uri);
+		const editor = await vscode.window.showTextDocument(document);
+		const selection = new vscode.Selection(5, 0, 5, 0);
+		editor.selection = selection;
+
+		const result = await subTestAtCursor(config, {functionName: 'TestMyFunction'});
+		assert.equal(result, undefined);
+	});
 });