[release] prepare v0.41.2 release

f5d6cadb extension/src/goStatus: fix quick pick for gopls trace open
53bf019e src/language: adjust panic trace capture logic
b4b68a76 README.md: use the new telemetry official doc link
361c8014 extension/src/goTelemetry: increase telemetry prompt rate to 1%
0b3fabc4 extension/src/goMain: skip vscgo installation on windows
9480a167 extension: start v0.42.0 dev cycle
beaf5fcb extension/test/gopls: allow more time for testify tests
484a195a extension/src/goTools: add golangci-lint and gofumpt version mapping
1d634bf3 .github/workflows: use 1.22, drop 1.18
6dd3745f docs/tools.md: fix vscgo package doc link
baab7be1 extension/tools/release: add test for rc
7bfbcaf1 src/goTest: fix multifile suite test fails to debug

Change-Id: I966aaac48ecb67b22824bd7b3600bf90d983a039
diff --git a/.github/workflows/test-long-all.yml b/.github/workflows/test-long-all.yml
index bee86cd..f924bc2 100644
--- a/.github/workflows/test-long-all.yml
+++ b/.github/workflows/test-long-all.yml
@@ -17,7 +17,7 @@
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
         version: ['stable', 'insiders']
-        go: ['1.18', '1.19', '1.20', '1.21']
+        go: ['1.19', '1.20', '1.21', '1.22']
 
     steps:
       - name: Clone repository
diff --git a/.github/workflows/test-long.yml b/.github/workflows/test-long.yml
index aff6b13..0027c8e 100644
--- a/.github/workflows/test-long.yml
+++ b/.github/workflows/test-long.yml
@@ -16,7 +16,7 @@
       matrix:
         os: [ubuntu-latest, windows-latest] # TODO: reenable macos-latest
         version: ['stable']
-        go: ['1.18', '1.19', '1.20', '1.21']
+        go: ['1.19', '1.20', '1.21', '1.22']
 
     steps:
       - name: Clone repository
diff --git a/.github/workflows/test-smoke.yml b/.github/workflows/test-smoke.yml
index c5f78fc..f6579e6 100644
--- a/.github/workflows/test-smoke.yml
+++ b/.github/workflows/test-smoke.yml
@@ -31,7 +31,7 @@
       - name: Setup Go
         uses: actions/setup-go@v4
         with:
-         go-version: '1.21'
+         go-version: '1.22'
          check-latest: true
          cache: true
 
diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml
index b213ec5..fa8357d 100644
--- a/.github/workflows/wiki.yml
+++ b/.github/workflows/wiki.yml
@@ -35,7 +35,7 @@
       - name: Setup Go
         uses: actions/setup-go@v4
         with:
-          go-version: '1.21'
+          go-version: '1.22'
           check-latest: true
           cache: true
           cache-dependency-path: '**/go.sum'
diff --git a/README.md b/README.md
index 630dc71..fc1e3fc 100644
--- a/README.md
+++ b/README.md
@@ -133,7 +133,7 @@
 
 ## Telemetry
 
-VS Code Go extension relies on the [Go Telemetry](https://telemetry.go.dev) to
+VS Code Go extension relies on the [Go Telemetry](https://go.dev/doc/telemetry) to
 learn insights about the performance and stability of the extension and the
 language server (`gopls`).
 **Go Telemetry data uploading is disabled by default** and can be enabled
@@ -171,9 +171,8 @@
 These are just a few ways that telemetry can improve gopls. The [telemetry blog
 post series](https://research.swtch.com/telemetry-uses) contains many more.
 
-Go telemetry is designed to be transparent and privacy-preserving. If you have
-concerns about enabling telemetry, you can learn more at
-[https://telemetry.go.dev/privacy](https://telemetry.go.dev/privacy).
+Go telemetry is designed to be transparent and privacy-preserving. Learn more at
+[https://go.dev/doc/telemetry](https://go.dev/doc/telemetry).
 
 ## Contributing
 
diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md
index 6f4f3db..89537d5 100644
--- a/extension/CHANGELOG.md
+++ b/extension/CHANGELOG.md
@@ -1,3 +1,10 @@
+## v0.41.2 - 14 Mar, 2024
+
+This release is a point release to increase the prompt rate of Go telemetry
+opt-in. Learn more at https://go.dev/doc/telemetry.
+For a detailed list of changes, refer to the complete
+[commit history](https://github.com/golang/vscode-go/compare/v0.41.1...v0.41.2).
+
 ## v0.41.1 - 22 Feb, 2024
 
 This release is a point release to skip `vscgo` installation on Windows
diff --git a/extension/package-lock.json b/extension/package-lock.json
index a3e7a1a..5dece0c 100644
--- a/extension/package-lock.json
+++ b/extension/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "go",
-  "version": "0.41.1",
+  "version": "0.41.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "go",
-      "version": "0.41.1",
+      "version": "0.41.2",
       "license": "MIT",
       "dependencies": {
         "diff": "4.0.2",
diff --git a/extension/package.json b/extension/package.json
index d6413d1..597d031 100644
--- a/extension/package.json
+++ b/extension/package.json
@@ -1,7 +1,7 @@
 {
   "name": "go",
   "displayName": "Go",
-  "version": "0.41.1",
+  "version": "0.41.2",
   "publisher": "golang",
   "description": "Rich Go language support for Visual Studio Code",
   "author": {
diff --git a/extension/src/goStatus.ts b/extension/src/goStatus.ts
index 91c57b6..d7a0809 100644
--- a/extension/src/goStatus.ts
+++ b/extension/src/goStatus.ts
@@ -106,7 +106,7 @@
 				case 'Choose Go Environment':
 					vscode.commands.executeCommand('go.environment.choose');
 					break;
-				case `${languageServerIcon}Open 'gopls' trace`:
+				case `${languageServerIcon} Open 'gopls' trace`:
 					if (serverOutputChannel) {
 						serverOutputChannel.show();
 					}
diff --git a/extension/src/goTelemetry.ts b/extension/src/goTelemetry.ts
index 58cdec9..081a5aa 100644
--- a/extension/src/goTelemetry.ts
+++ b/extension/src/goTelemetry.ts
@@ -162,7 +162,7 @@
 	async promptForTelemetry(
 		isPreviewExtension: boolean,
 		isVSCodeTelemetryEnabled: boolean = vscode.env.isTelemetryEnabled,
-		samplingInterval = 1 /* prompt N out of 1000. 1 = 0.1% */
+		samplingInterval = 10 /* prompt N out of 1000. 10 = 1% */
 	) {
 		if (!this.active) return;
 
diff --git a/extension/src/goTest.ts b/extension/src/goTest.ts
index 5d089bc..edc2460 100644
--- a/extension/src/goTest.ts
+++ b/extension/src/goTest.ts
@@ -18,10 +18,12 @@
 	getBenchmarkFunctions,
 	getTestFlags,
 	getTestFunctionDebugArgs,
-	getTestFunctions,
+	getTestFunctionsAndTestSuite,
 	getTestTags,
 	goTest,
-	TestConfig
+	TestConfig,
+	SuiteToTestMap,
+	getTestFunctions
 } from './testUtils';
 
 // lastTestConfig holds a reference to the last executed TestConfig which allows
@@ -52,8 +54,11 @@
 		throw new NotFoundError('No tests found. Current file is not a test file.');
 	}
 
-	const getFunctions = cmd === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
-	const testFunctions = (await getFunctions(goCtx, editor.document)) ?? [];
+	const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
+		cmd === 'benchmark',
+		goCtx,
+		editor.document
+	);
 	// We use functionName if it was provided as argument
 	// Otherwise find any test function containing the cursor.
 	const testFunctionName =
@@ -67,9 +72,9 @@
 	await editor.document.save();
 
 	if (cmd === 'debug') {
-		return debugTestAtCursor(editor, testFunctionName, testFunctions, goConfig);
+		return debugTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig);
 	} else if (cmd === 'benchmark' || cmd === 'test') {
-		return runTestAtCursor(editor, testFunctionName, testFunctions, goConfig, cmd, args);
+		return runTestAtCursor(editor, testFunctionName, testFunctions, suiteToTest, goConfig, cmd, args);
 	} else {
 		throw new Error(`Unsupported command: ${cmd}`);
 	}
@@ -92,7 +97,7 @@
 	}
 
 	await editor.document.save();
-	const testFunctions = (await getTestFunctions(goCtx, editor.document)) ?? [];
+	const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(false, goCtx, editor.document);
 	// 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));
@@ -142,9 +147,9 @@
 	const escapedName = escapeSubTestName(testFunctionName, subTestName);
 
 	if (cmd === 'debug') {
-		return debugTestAtCursor(editor, escapedName, testFunctions, goConfig);
+		return debugTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig);
 	} else if (cmd === 'test') {
-		return runTestAtCursor(editor, escapedName, testFunctions, goConfig, cmd, args);
+		return runTestAtCursor(editor, escapedName, testFunctions, suiteToTest, goConfig, cmd, args);
 	} else {
 		throw new Error(`Unsupported command: ${cmd}`);
 	}
@@ -160,7 +165,7 @@
 export function testAtCursor(cmd: TestAtCursorCmd): CommandFactory {
 	return (ctx, goCtx) => (args: any) => {
 		const goConfig = getGoConfig();
-		_testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
+		return _testAtCursor(goCtx, goConfig, cmd, args).catch((err) => {
 			if (err instanceof NotFoundError) {
 				vscode.window.showInformationMessage(err.message);
 			} else {
@@ -202,13 +207,14 @@
 	editor: vscode.TextEditor,
 	testFunctionName: string,
 	testFunctions: vscode.DocumentSymbol[],
+	suiteToTest: SuiteToTestMap,
 	goConfig: vscode.WorkspaceConfiguration,
 	cmd: TestAtCursorCmd,
 	args: any
 ) {
 	const testConfigFns = [testFunctionName];
 	if (cmd !== 'benchmark' && extractInstanceTestName(testFunctionName)) {
-		testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions).map((t) => t.name));
+		testConfigFns.push(...findAllTestSuiteRuns(editor.document, testFunctions, suiteToTest).map((t) => t.name));
 	}
 
 	const isMod = await isModSupported(editor.document.uri);
@@ -259,11 +265,12 @@
 	editorOrDocument: vscode.TextEditor | vscode.TextDocument,
 	testFunctionName: string,
 	testFunctions: vscode.DocumentSymbol[],
+	suiteToFunc: SuiteToTestMap,
 	goConfig: vscode.WorkspaceConfiguration,
 	sessionID?: string
 ) {
 	const doc = 'document' in editorOrDocument ? editorOrDocument.document : editorOrDocument;
-	const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions);
+	const args = getTestFunctionDebugArgs(doc, testFunctionName, testFunctions, suiteToFunc);
 	const tags = getTestTags(goConfig);
 	const buildFlags = tags ? ['-tags', tags] : [];
 	const flagsFromConfig = getTestFlags(goConfig);
diff --git a/extension/src/goTest/run.ts b/extension/src/goTest/run.ts
index eb0f03e..aff9fbe 100644
--- a/extension/src/goTest/run.ts
+++ b/extension/src/goTest/run.ts
@@ -21,7 +21,7 @@
 import { outputChannel } from '../goStatus';
 import { isModSupported } from '../goModules';
 import { getGoConfig } from '../config';
-import { getBenchmarkFunctions, getTestFlags, getTestFunctions, goTest, GoTestOutput } from '../testUtils';
+import { getTestFlags, getTestFunctionsAndTestSuite, goTest, GoTestOutput } from '../testUtils';
 import { GoTestResolver } from './resolve';
 import { dispose, forEachAsync, GoTest, Workspace } from './utils';
 import { GoTestProfiler, ProfilingOptions } from './profile';
@@ -161,8 +161,11 @@
 		await doc.save();
 
 		const goConfig = getGoConfig(test.uri);
-		const getFunctions = kind === 'benchmark' ? getBenchmarkFunctions : getTestFunctions;
-		const testFunctions = await getFunctions(this.goCtx, doc, token);
+		const { testFunctions, suiteToTest } = await getTestFunctionsAndTestSuite(
+			kind === 'benchmark',
+			this.goCtx,
+			doc
+		);
 
 		// TODO Can we get output from the debug session, in order to check for
 		// run/pass/fail events?
@@ -191,7 +194,8 @@
 
 		const run = this.ctrl.createTestRun(request, `Debug ${name}`);
 		if (!testFunctions) return;
-		const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, goConfig, id);
+		const started = await debugTestAtCursor(doc, escapeSubTestName(name), testFunctions, suiteToTest, goConfig, id);
+
 		if (!started) {
 			subs.forEach((s) => s.dispose());
 			run.end();
diff --git a/extension/src/goTools.ts b/extension/src/goTools.ts
index b22eb82..9ba83cc 100644
--- a/extension/src/goTools.ts
+++ b/extension/src/goTools.ts
@@ -75,10 +75,13 @@
 	}
 	if (tool.name === 'gofumpt') {
 		if (goVersion.lt('1.18')) return importPath + '@v0.2.1';
+		if (goVersion.lt('1.19')) return importPath + '@v0.4.0';
+		if (goVersion.lt('1.20')) return importPath + '@v0.5.0';
 	}
 	if (tool.name === 'golangci-lint') {
 		if (goVersion.lt('1.18')) return importPath + '@v1.47.3';
 		if (goVersion.lt('1.20')) return importPath + '@v1.53.3';
+		if (goVersion.lt('1.21')) return importPath + '@v1.55.2';
 	}
 	if (tool.defaultVersion) {
 		return importPath + '@' + tool.defaultVersion;
diff --git a/extension/src/goToolsInformation.ts b/extension/src/goToolsInformation.ts
index 32c4261..2bcd409 100644
--- a/extension/src/goToolsInformation.ts
+++ b/extension/src/goToolsInformation.ts
@@ -39,7 +39,7 @@
 		replacedByGopls: true,
 		isImportant: false,
 		description: 'Formatter',
-		defaultVersion: 'v0.5.0'
+		defaultVersion: 'v0.6.0'
 	},
 	'goimports': {
 		name: 'goimports',
diff --git a/extension/src/language/goLanguageServer.ts b/extension/src/language/goLanguageServer.ts
index 9f5352f..1f32c3c 100644
--- a/extension/src/language/goLanguageServer.ts
+++ b/extension/src/language/goLanguageServer.ts
@@ -1530,7 +1530,6 @@
 enum GoplsFailureModes {
 	NO_GOPLS_LOG = 'no gopls log',
 	EMPTY_PANIC_TRACE = 'empty panic trace',
-	INCOMPLETE_PANIC_TRACE = 'incomplete panic trace',
 	INCORRECT_COMMAND_USAGE = 'incorrect gopls command usage',
 	UNRECOGNIZED_CRASH_PATTERN = 'unrecognized crash pattern'
 }
@@ -1544,34 +1543,40 @@
 	const panicMsgBegin = logs.lastIndexOf('panic: ');
 	if (panicMsgBegin > -1) {
 		// panic message was found.
-		const panicMsgEnd = logs.indexOf('Connection to server got closed.', panicMsgBegin);
+		let panicTrace = logs.substr(panicMsgBegin);
+		const panicMsgEnd = panicTrace.search(/\[(Info|Warning|Error)\s+-\s+/);
 		if (panicMsgEnd > -1) {
-			const panicTrace = logs.substr(panicMsgBegin, panicMsgEnd - panicMsgBegin);
-			const filePattern = /(\S+\.go):\d+/;
-			const sanitized = panicTrace
-				.split('\n')
-				.map((line: string) => {
-					// Even though this is a crash from gopls, the file path
-					// can contain user names and user's filesystem directory structure.
-					// We can still locate the corresponding file if the file base is
-					// available because the full package path is part of the function
-					// name. So, leave only the file base.
-					const m = line.match(filePattern);
-					if (!m) {
-						return line;
-					}
-					const filePath = m[1];
-					const fileBase = path.basename(filePath);
-					return line.replace(filePath, '  ' + fileBase);
-				})
-				.join('\n');
-
-			if (sanitized) {
-				return { sanitizedLog: sanitized };
-			}
-			return { failureReason: GoplsFailureModes.EMPTY_PANIC_TRACE };
+			panicTrace = panicTrace.substr(0, panicMsgEnd);
 		}
-		return { failureReason: GoplsFailureModes.INCOMPLETE_PANIC_TRACE };
+		const filePattern = /(\S+\.go):\d+/;
+		const sanitized = panicTrace
+			.split('\n')
+			.map((line: string) => {
+				// Even though this is a crash from gopls, the file path
+				// can contain user names and user's filesystem directory structure.
+				// We can still locate the corresponding file if the file base is
+				// available because the full package path is part of the function
+				// name. So, leave only the file base.
+				const m = line.match(filePattern);
+				if (!m) {
+					return line;
+				}
+				const filePath = m[1];
+				const fileBase = path.basename(filePath);
+				return line.replace(filePath, '  ' + fileBase);
+			})
+			.join('\n');
+
+		if (sanitized) {
+			return { sanitizedLog: sanitized };
+		}
+		return { failureReason: GoplsFailureModes.EMPTY_PANIC_TRACE };
+	}
+	// Capture Fatal
+	//    foo.go:1: the last message (caveat - we capture only the first log line)
+	const m = logs.match(/(^\S+\.go:\d+:.*$)/gm);
+	if (m && m.length > 0) {
+		return { sanitizedLog: m[0].toString() };
 	}
 	const initFailMsgBegin = logs.lastIndexOf('gopls client:');
 	if (initFailMsgBegin > -1) {
@@ -1590,13 +1595,6 @@
 	if (logs.lastIndexOf('Usage:') > -1) {
 		return { failureReason: GoplsFailureModes.INCORRECT_COMMAND_USAGE };
 	}
-	// Capture Fatal
-	//    foo.go:1: the last message (caveat - we capture only the first log line)
-	const m = logs.match(/(^\S+\.go:\d+:.*$)/gm);
-	if (m && m.length > 0) {
-		return { sanitizedLog: m[0].toString() };
-	}
-
 	return { failureReason: GoplsFailureModes.UNRECOGNIZED_CRASH_PATTERN };
 }
 
@@ -1664,6 +1662,6 @@
 	} catch (e) {
 		const duration = new Date().getTime() - start.getTime();
 		console.log(`gopls stats -anon failed: ${JSON.stringify(e)}`);
-		return `gopls stats -anon failed after running for ${duration}ms`; // e may contain user information. don't include in the report.
+		return `gopls stats -anon failed after ${duration} ms. Please check if gopls is killed by OS.`;
 	}
 }
diff --git a/extension/src/testUtils.ts b/extension/src/testUtils.ts
index 0b72d9f..4070f6e 100644
--- a/extension/src/testUtils.ts
+++ b/extension/src/testUtils.ts
@@ -11,6 +11,7 @@
 import path = require('path');
 import util = require('util');
 import vscode = require('vscode');
+import { promises as fs } from 'fs';
 
 import { applyCodeCoverageToAllEditors } from './goCover';
 import { toolExecutionEnvironment } from './goEnv';
@@ -50,6 +51,7 @@
 const benchmarkRegex = /^Benchmark$|^Benchmark\P{Ll}.*/u;
 const fuzzFuncRegx = /^Fuzz$|^Fuzz\P{Ll}.*/u;
 const testMainRegex = /TestMain\(.*\*testing.M\)/;
+const runTestSuiteRegex = /^\s*suite\.Run\(\w+,\s*(?:&?(?<type1>\w+)\{|new\((?<type2>\w+)\))/mu;
 
 /**
  * Input to goTest.
@@ -153,27 +155,76 @@
 	doc: vscode.TextDocument,
 	token?: vscode.CancellationToken
 ): Promise<vscode.DocumentSymbol[] | undefined> {
+	const result = await getTestFunctionsAndTestifyHint(goCtx, doc, token);
+	return result.testFunctions;
+}
+
+/**
+ * Returns all Go unit test functions in the given source file and an hint if testify is used.
+ *
+ * @param doc A Go source file
+ */
+export async function getTestFunctionsAndTestifyHint(
+	goCtx: GoExtensionContext,
+	doc: vscode.TextDocument,
+	token?: vscode.CancellationToken
+): Promise<{ testFunctions?: vscode.DocumentSymbol[]; foundTestifyTestFunction?: boolean }> {
 	const documentSymbolProvider = GoDocumentSymbolProvider(goCtx, true);
 	const symbols = await documentSymbolProvider.provideDocumentSymbols(doc);
 	if (!symbols || symbols.length === 0) {
-		return;
+		return {};
 	}
 	const symbol = symbols[0];
 	if (!symbol) {
-		return;
+		return {};
 	}
 	const children = symbol.children;
 
-	// With gopls dymbol provider symbols, the symbols have the imports of all
+	// With gopls symbol provider, the symbols have the imports of all
 	// the package, so suite tests from all files will be found.
 	const testify = importsTestify(symbols);
-	return children.filter(
+
+	const allTestFunctions = children.filter(
 		(sym) =>
-			(sym.kind === vscode.SymbolKind.Function || sym.kind === vscode.SymbolKind.Method) &&
+			sym.kind === vscode.SymbolKind.Function &&
 			// Skip TestMain(*testing.M) - see https://github.com/golang/vscode-go/issues/482
 			!testMainRegex.test(doc.lineAt(sym.range.start.line).text) &&
-			(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name) || (testify && testMethodRegex.test(sym.name)))
+			(testFuncRegex.test(sym.name) || fuzzFuncRegx.test(sym.name))
 	);
+
+	const allTestMethods = testify
+		? children.filter((sym) => sym.kind === vscode.SymbolKind.Method && testMethodRegex.test(sym.name))
+		: [];
+
+	return {
+		testFunctions: allTestFunctions.concat(allTestMethods),
+		foundTestifyTestFunction: allTestMethods.length > 0
+	};
+}
+
+/**
+ * Returns all the Go test functions (or benchmark) from the given Go source file, and the associated test suites when testify is used.
+ *
+ * @param doc A Go source file
+ */
+export async function getTestFunctionsAndTestSuite(
+	isBenchmark: boolean,
+	goCtx: GoExtensionContext,
+	doc: vscode.TextDocument
+): Promise<{ testFunctions: vscode.DocumentSymbol[]; suiteToTest: SuiteToTestMap }> {
+	if (isBenchmark) {
+		return {
+			testFunctions: (await getBenchmarkFunctions(goCtx, doc)) ?? [],
+			suiteToTest: {}
+		};
+	}
+
+	const { testFunctions, foundTestifyTestFunction } = await getTestFunctionsAndTestifyHint(goCtx, doc);
+
+	return {
+		testFunctions: testFunctions ?? [],
+		suiteToTest: foundTestifyTestFunction ? await getSuiteToTestMap(goCtx, doc) : {}
+	};
 }
 
 /**
@@ -199,17 +250,16 @@
 export function getTestFunctionDebugArgs(
 	document: vscode.TextDocument,
 	testFunctionName: string,
-	testFunctions: vscode.DocumentSymbol[]
+	testFunctions: vscode.DocumentSymbol[],
+	suiteToFunc: SuiteToTestMap
 ): string[] {
 	if (benchmarkRegex.test(testFunctionName)) {
 		return ['-test.bench', '^' + testFunctionName + '$', '-test.run', 'a^'];
 	}
 	const instanceMethod = extractInstanceTestName(testFunctionName);
 	if (instanceMethod) {
-		const testFns = findAllTestSuiteRuns(document, testFunctions);
-		const testSuiteRuns = ['-test.run', `^${testFns.map((t) => t.name).join('|')}$`];
-		const testSuiteTests = ['-testify.m', `^${instanceMethod}$`];
-		return [...testSuiteRuns, ...testSuiteTests];
+		const testFns = findAllTestSuiteRuns(document, testFunctions, suiteToFunc);
+		return ['-test.run', `^${testFns.map((t) => t.name).join('|')}$/^${instanceMethod}$`];
 	} else {
 		return ['-test.run', `^${testFunctionName}$`];
 	}
@@ -222,12 +272,22 @@
  */
 export function findAllTestSuiteRuns(
 	doc: vscode.TextDocument,
-	allTests: vscode.DocumentSymbol[]
+	allTests: vscode.DocumentSymbol[],
+	suiteToFunc: SuiteToTestMap
 ): vscode.DocumentSymbol[] {
-	// get non-instance test functions
-	const testFunctions = allTests?.filter((t) => !testMethodRegex.test(t.name));
-	// filter further to ones containing suite.Run()
-	return testFunctions?.filter((t) => doc.getText(t.range).includes('suite.Run(')) ?? [];
+	const suites = allTests
+		// Find all tests with receivers.
+		?.map((e) => e.name.match(testMethodRegex))
+		.filter((e) => e?.length === 3)
+		// Take out receiever, strip leading *.
+		.map((e) => e && e[1].replace(/^\*/g, ''))
+		// Map receiver name to test that runs "suite.Run".
+		.map((e) => e && suiteToFunc[e])
+		// Filter out empty results.
+		.filter((e): e is vscode.DocumentSymbol => !!e);
+
+	// Dedup.
+	return [...new Set(suites)];
 }
 
 /**
@@ -254,6 +314,59 @@
 	return children.filter((sym) => sym.kind === vscode.SymbolKind.Function && benchmarkRegex.test(sym.name));
 }
 
+export type SuiteToTestMap = Record<string, vscode.DocumentSymbol>;
+
+/**
+ * Returns a mapping between a package's function receivers to
+ * the test method that initiated them with "suite.Run".
+ *
+ * @param the URI of a Go source file.
+ * @return function symbols from all source files of the package, mapped by target suite names.
+ */
+export async function getSuiteToTestMap(
+	goCtx: GoExtensionContext,
+	doc: vscode.TextDocument,
+	token?: vscode.CancellationToken
+) {
+	// Get all the package documents.
+	const packageDir = path.parse(doc.fileName).dir;
+	const packageContent = await fs.readdir(packageDir, { withFileTypes: true });
+	const packageFilenames = packageContent
+		// Only go files.
+		.filter((dirent) => dirent.isFile())
+		.map((dirent) => dirent.name)
+		.filter((name) => name.endsWith('.go'));
+	const packageDocs = await Promise.all(
+		packageFilenames.map((e) => path.join(packageDir, e)).map(vscode.workspace.openTextDocument)
+	);
+
+	const suiteToTest: SuiteToTestMap = {};
+	for (const packageDoc of packageDocs) {
+		const funcs = await getTestFunctions(goCtx, packageDoc, token);
+		if (!funcs) {
+			continue;
+		}
+
+		for (const func of funcs) {
+			const funcText = packageDoc.getText(func.range);
+
+			// Matches run suites of the types:
+			// type1: suite.Run(t, MySuite{
+			// type1: suite.Run(t, &MySuite{
+			// type2: suite.Run(t, new(MySuite)
+			const matchRunSuite = funcText.match(runTestSuiteRegex);
+			if (!matchRunSuite) {
+				continue;
+			}
+
+			const g = matchRunSuite.groups;
+			suiteToTest[g?.type1 || g?.type2 || ''] = func;
+		}
+	}
+
+	return suiteToTest;
+}
+
 /**
  * go test -json output format.
  * which is a subset of https://golang.org/cmd/test2json/#hdr-Output_Format
diff --git a/extension/src/welcome.ts b/extension/src/welcome.ts
index 2154acf..b0afe12 100644
--- a/extension/src/welcome.ts
+++ b/extension/src/welcome.ts
@@ -197,9 +197,7 @@
 				</p>
 				<p>
 				If we get enough adoption, this data can significantly advance the pace of the go plugin development and help us meet a higher standard of reliability.
-                Go telemetry is designed to be transparent and privacy-preserving.
-				If you have concerns about enabling telemetry, you can learn more at
-				<a href="https://telemetry.go.dev/privacy">Go Telemetry Privacy Policy</a>.
+                Go telemetry is designed to be transparent and privacy-preserving. Learn more at <a href="https://go.dev/doc/telemetry">https://go.dev/doc/telemetry</a>.
 				</p>
 			</div>
 
diff --git a/extension/test/gopls/codelens.test.ts b/extension/test/gopls/codelens.test.ts
index 31c1fcd..988adab 100644
--- a/extension/test/gopls/codelens.test.ts
+++ b/extension/test/gopls/codelens.test.ts
@@ -11,9 +11,10 @@
 import vscode = require('vscode');
 import { updateGoVarsFromConfig } from '../../src/goInstallTools';
 import { GoRunTestCodeLensProvider } from '../../src/goRunTestCodelens';
-import { subTestAtCursor } from '../../src/goTest';
+import { subTestAtCursor, testAtCursor } from '../../src/goTest';
 import { MockExtensionContext } from '../mocks/MockContext';
 import { Env } from './goplsTestEnv.utils';
+import * as testUtils from '../../src/testUtils';
 
 suite('Code lenses for testing and benchmarking', function () {
 	this.timeout(20000);
@@ -200,4 +201,145 @@
 		// Results should match `go test -list`.
 		assert.deepStrictEqual(found, ['TestNotMain']);
 	});
+
+	test('Debug - debugs a test with cursor on t.Run line', async () => {
+		const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
+
+		const editor = await vscode.window.showTextDocument(document);
+		editor.selection = new vscode.Selection(7, 4, 7, 4);
+		const result = await subTestAtCursor('debug')(ctx, env.goCtx)([]);
+		assert.strictEqual(result, true);
+
+		assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
+		const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
+		gotConfig.program = '';
+		assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
+			name: 'Debug Test',
+			type: 'go',
+			request: 'launch',
+			args: ['-test.run', '^TestSample$/^sample_test_passing$'],
+			buildFlags: '',
+			env: {},
+			sessionID: undefined,
+			mode: 'test',
+			envFile: null,
+			program: ''
+		});
+	});
+});
+
+suite('Code lenses with stretchr/testify/suite', function () {
+	if (process.platform === 'win32') {
+		this.timeout(20000); // Gopls on windows needs more time to load required modules.
+	}
+
+	const ctx = MockExtensionContext.new();
+
+	const testdataDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'stretchrTestSuite');
+	const env = new Env();
+
+	this.afterEach(async function () {
+		// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
+		// I don't know why but this.currentTest.state does not have the expected value when
+		// used with teardown.
+		env.flushTrace(this.currentTest?.state === 'failed');
+		ctx.teardown();
+		sinon.restore();
+	});
+
+	suiteSetup(async () => {
+		await updateGoVarsFromConfig({});
+		await env.startGopls(undefined, undefined, testdataDir);
+	});
+
+	suiteTeardown(async () => {
+		await env.teardown();
+	});
+
+	test('Run test at cursor', async () => {
+		const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true));
+
+		const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go')));
+		editor.selection = new vscode.Selection(25, 4, 25, 4);
+
+		const result = await testAtCursor('test')(ctx, env.goCtx)([]);
+		assert.strictEqual(result, true);
+
+		assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest');
+		const gotConfig = goTestStub.getCall(0).args[0];
+		assert.deepStrictEqual(gotConfig.functions, ['(*ExampleTestSuite).TestExample', 'TestExampleTestSuite']);
+	});
+
+	test('Run test at cursor in different file than test suite definition', async () => {
+		const goTestStub = sinon.stub(testUtils, 'goTest').returns(Promise.resolve(true));
+
+		const editor = await vscode.window.showTextDocument(
+			vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go'))
+		);
+		editor.selection = new vscode.Selection(3, 4, 3, 4);
+
+		const result = await testAtCursor('test')(ctx, env.goCtx)([]);
+		assert.strictEqual(result, true);
+
+		assert.strictEqual(goTestStub.callCount, 1, 'expected one call to goTest');
+		const gotConfig = goTestStub.getCall(0).args[0];
+		assert.deepStrictEqual(gotConfig.functions, [
+			'(*ExampleTestSuite).TestExampleInAnotherFile',
+			'TestExampleTestSuite'
+		]);
+	});
+
+	test('Debug test at cursor', async () => {
+		const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
+
+		const editor = await vscode.window.showTextDocument(vscode.Uri.file(path.join(testdataDir, 'suite_test.go')));
+		editor.selection = new vscode.Selection(25, 4, 25, 4);
+
+		const result = await testAtCursor('debug')(ctx, env.goCtx)([]);
+		assert.strictEqual(result, true);
+
+		assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
+		const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
+		gotConfig.program = '';
+		assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
+			name: 'Debug Test',
+			type: 'go',
+			request: 'launch',
+			args: ['-test.run', '^TestExampleTestSuite$/^TestExample$'],
+			buildFlags: '',
+			env: {},
+			sessionID: undefined,
+			mode: 'test',
+			envFile: null,
+			program: ''
+		});
+	});
+
+	test('Debug test at cursor in different file than test suite definition', async () => {
+		const startDebuggingStub = sinon.stub(vscode.debug, 'startDebugging').returns(Promise.resolve(true));
+
+		const editor = await vscode.window.showTextDocument(
+			vscode.Uri.file(path.join(testdataDir, 'another_suite_test.go'))
+		);
+		editor.selection = new vscode.Selection(3, 4, 3, 4);
+
+		const result = await testAtCursor('debug')(ctx, env.goCtx)([]);
+		assert.strictEqual(result, true);
+
+		assert.strictEqual(startDebuggingStub.callCount, 1, 'expected one call to startDebugging');
+		const gotConfig = startDebuggingStub.getCall(0).args[1] as vscode.DebugConfiguration;
+		gotConfig.program = '';
+		assert.deepStrictEqual<vscode.DebugConfiguration>(gotConfig, {
+			name: 'Debug Test',
+			type: 'go',
+			request: 'launch',
+			args: ['-test.run', '^TestExampleTestSuite$/^TestExampleInAnotherFile$'],
+			buildFlags: '',
+			env: {},
+			sessionID: undefined,
+			mode: 'test',
+			envFile: null,
+			program: ''
+		});
+	});
 });
diff --git a/extension/test/gopls/goTest.run.test.ts b/extension/test/gopls/goTest.run.test.ts
index 4a75322..35b249c 100644
--- a/extension/test/gopls/goTest.run.test.ts
+++ b/extension/test/gopls/goTest.run.test.ts
@@ -8,12 +8,17 @@
 import { MockExtensionContext } from '../mocks/MockContext';
 import { GoTest } from '../../src/goTest/utils';
 import { Env } from './goplsTestEnv.utils';
+import { updateGoVarsFromConfig } from '../../src/goInstallTools';
 
 suite('Go Test Runner', () => {
 	const fixtureDir = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
 
 	let testExplorer: GoTestExplorer;
 
+	suiteSetup(async () => {
+		await updateGoVarsFromConfig({});
+	});
+
 	suite('parseOutput', () => {
 		const ctx = MockExtensionContext.new();
 		suiteSetup(async () => {
diff --git a/extension/test/gopls/report.test.ts b/extension/test/gopls/report.test.ts
index 4cabab9..186378b 100644
--- a/extension/test/gopls/report.test.ts
+++ b/extension/test/gopls/report.test.ts
@@ -26,9 +26,14 @@
 				want: sanitizedTraceFromIssueVSCodeGo572LSP317
 			},
 			{
+				name: 'panic trace 2024 March',
+				in: trace2024MarchPanic,
+				want: sanitizedTrace2024MarchPanic
+			},
+			{
 				name: 'incomplete panic trace',
-				in: 'panic: \nsecret\n',
-				wantReason: 'incomplete panic trace'
+				in: 'panic: \ntruncated\n',
+				want: 'panic: \ntruncated\n'
 			},
 			{
 				name: 'incomplete initialization error message',
@@ -42,7 +47,7 @@
 			assert.strictEqual(
 				JSON.stringify(sanitizedLog),
 				JSON.stringify(tc.want),
-				`sanitizeGoplsTrace(${tc.name}) returned unexpected sanitizedLog result`
+				`sanitizeGoplsTrace(${tc.name}) returned unexpected sanitizedLog result - ${sanitizedLog}`
 			);
 			assert.strictEqual(
 				failureReason,
@@ -315,7 +320,7 @@
   handler.go:103 +0x86
 created by golang.org/x/tools/internal/jsonrpc2.AsyncHandler.func1
   handler.go:100 +0x171
-[Info - 12:50:26 PM] `;
+`;
 
 const traceFromIssueVSCodeGo572LSP317 = `
 [Error - 12:20:35 PM] Stopping server failed
@@ -335,3 +340,47 @@
 const sanitizedTraceFromIssueVSCodeGo572LSP317 = `gopls client: couldn't create connection to server.
   Message: Socket closed before the connection was established
   Code: -32099 `;
+
+const trace2024MarchPanic = `
+[Info  - 9:58:40 AM] 
+true
+[Error - 9:58:40 AM] gopls client: couldn't create connection to server.
+  Message: Pending response rejected since connection got disposed
+  Code: -32097 
+panic: crash
+
+goroutine 1 [running]:
+golang.org/x/tools/gopls/internal/cmd.(*Serve).Run(0xc000486310?, {0xc0000b8090?, 0x0?}, {0x0?, 0x0?, 0x0?})
+	/Users/Gopher/projects/tools/gopls/internal/cmd/serve.go:81 +0x25
+golang.org/x/tools/internal/tool.Run({0x1012d048, 0xc00019c3f0}, 0xc000486310, {0x1012f9e0, 0xc000159b40}, {0xc0000b8090, 0x0, 0x0})
+	/Users/Gopher/projects/tools/internal/tool/tool.go:192 +0x691
+golang.org/x/tools/gopls/internal/cmd.(*Application).Run(0xc000159b00, {0x1012d010, 0x107d9840}, {0xc0000b8090, 0x0, 0x0})
+	/Users/Gopher/projects/tools/gopls/internal/cmd/cmd.go:240 +0x147
+golang.org/x/tools/internal/tool.Run({0x1012d010, 0x107d9840}, 0xc0004862a0, {0x1012f3a0, 0xc000159b00}, {0xc0000b8060, 0x4, 0x4})
+	/Users/Gopher/projects/tools/internal/tool/tool.go:192 +0x691
+golang.org/x/tools/internal/tool.Main({0x1012d010, 0x107d9840}, {0x1012f3a0, 0xc000159b00}, {0xc0000b8060, 0x4, 0x4})
+	/Users/Gopher/projects/tools/internal/tool/tool.go:93 +0x12a
+main.main()
+	/Users/Gopher/projects/tools/gopls/main.go:34 +0x109
+[Error - 9:58:49 AM] 
+[Error - 9:58:49 AM] gopls client: couldn't create connection to server.
+  Message: Pending response rejected since connection got disposed
+  Code: -32097 
+Error starting language server: Error: Pending response rejected since connection got disposed`;
+
+const sanitizedTrace2024MarchPanic = `panic: crash
+
+goroutine 1 [running]:
+golang.org/x/tools/gopls/internal/cmd.(*Serve).Run(0xc000486310?, {0xc0000b8090?, 0x0?}, {0x0?, 0x0?, 0x0?})
+	  serve.go:81 +0x25
+golang.org/x/tools/internal/tool.Run({0x1012d048, 0xc00019c3f0}, 0xc000486310, {0x1012f9e0, 0xc000159b40}, {0xc0000b8090, 0x0, 0x0})
+	  tool.go:192 +0x691
+golang.org/x/tools/gopls/internal/cmd.(*Application).Run(0xc000159b00, {0x1012d010, 0x107d9840}, {0xc0000b8090, 0x0, 0x0})
+	  cmd.go:240 +0x147
+golang.org/x/tools/internal/tool.Run({0x1012d010, 0x107d9840}, 0xc0004862a0, {0x1012f3a0, 0xc000159b00}, {0xc0000b8060, 0x4, 0x4})
+	  tool.go:192 +0x691
+golang.org/x/tools/internal/tool.Main({0x1012d010, 0x107d9840}, {0x1012f3a0, 0xc000159b00}, {0xc0000b8060, 0x4, 0x4})
+	  tool.go:93 +0x12a
+main.main()
+	  main.go:34 +0x109
+`;
diff --git a/extension/test/integration/install.test.ts b/extension/test/integration/install.test.ts
index ce435cf..4719cb0 100644
--- a/extension/test/integration/install.test.ts
+++ b/extension/test/integration/install.test.ts
@@ -205,19 +205,18 @@
 	const gofumptDefault = allToolsInformation['gofumpt'].defaultVersion!;
 	test('Install gofumpt with old go', async () => {
 		await runTest(
-			[{ name: 'gofumpt', versions: ['v0.2.1', gofumptDefault], wantVersion: 'v0.2.1' }],
+			[{ name: 'gofumpt', versions: ['v0.4.0', 'v0.5.0', gofumptDefault], wantVersion: 'v0.5.0' }],
 			true, // LOCAL PROXY
 			true, // GOBIN
-			'go1.17' // Go Version
+			'go1.19' // Go Version
 		);
 	});
-
 	test('Install gofumpt with new go', async () => {
 		await runTest(
-			[{ name: 'gofumpt', versions: ['v0.2.1', gofumptDefault], wantVersion: gofumptDefault }],
+			[{ name: 'gofumpt', versions: ['v0.4.0', 'v0.5.0', gofumptDefault], wantVersion: gofumptDefault }],
 			true, // LOCAL PROXY
 			true, // GOBIN
-			'go1.18' // Go Version
+			'go1.22' // Go Version
 		);
 	});
 	test('Install all tools via GOPROXY', async () => {
diff --git a/extension/test/integration/test.test.ts b/extension/test/integration/test.test.ts
index 1ca2431..500b176 100644
--- a/extension/test/integration/test.test.ts
+++ b/extension/test/integration/test.test.ts
@@ -97,6 +97,19 @@
 			flags: ['-run', 'TestC']
 		});
 	});
+	test('use -testify.m for methods', () => {
+		runTest({
+			expectedArgs:
+				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
+			expectedOutArgs:
+				'test -timeout 30s -run ^TestExampleTestSuite$ -testify.m ^(TestExample|TestAnotherExample)$ ./...',
+			functions: [
+				'(*ExampleTestSuite).TestExample',
+				'(*ExampleTestSuite).TestAnotherExample',
+				'TestExampleTestSuite'
+			]
+		});
+	});
 });
 
 suite('Test Go Test', function () {
diff --git a/extension/test/testdata/stretchrTestSuite/another_suite_test.go b/extension/test/testdata/stretchrTestSuite/another_suite_test.go
new file mode 100644
index 0000000..1af57e7
--- /dev/null
+++ b/extension/test/testdata/stretchrTestSuite/another_suite_test.go
@@ -0,0 +1,7 @@
+package main_test
+
+func (suite *ExampleTestSuite) TestExampleInAnotherFile() {
+	if suite.VariableThatShouldStartAtFive != 5 {
+		suite.T().Fatalf("%d != %d", 5, suite.VariableThatShouldStartAtFive)
+	}
+}
diff --git a/extension/test/testdata/stretchrTestSuite/go.mod b/extension/test/testdata/stretchrTestSuite/go.mod
index 272ea9c..7703985 100644
--- a/extension/test/testdata/stretchrTestSuite/go.mod
+++ b/extension/test/testdata/stretchrTestSuite/go.mod
@@ -1,10 +1,13 @@
 module example/a
 
-go 1.16
+go 1.21
+
+require github.com/stretchr/testify v1.7.0
 
 require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/kr/pretty v0.1.0 // indirect
-	github.com/stretchr/testify v1.7.0
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
 )
diff --git a/extension/tools/release/release_test.go b/extension/tools/release/release_test.go
index 66c6f6e..f5437ed 100644
--- a/extension/tools/release/release_test.go
+++ b/extension/tools/release/release_test.go
@@ -27,17 +27,19 @@
 	moduleRoot := string(bytes.TrimSpace(out))
 
 	for _, command := range []string{"package", "publish"} {
-		t.Run(command, func(t *testing.T) {
-			testRelease(t, moduleRoot, command)
-		})
+		for _, tagName := range []string{"v0.0.0", "v0.0.0-rc.1"} {
+			t.Run(command+"-"+tagName, func(t *testing.T) {
+				testRelease(t, moduleRoot, command, tagName)
+			})
+		}
 	}
 }
 
-func testRelease(t *testing.T, moduleRoot, command string) {
+func testRelease(t *testing.T, moduleRoot, command, tagName string) {
 	cmd := exec.Command("go", "run", "-C", moduleRoot, "tools/release/release.go", "-n", command)
 	cmd.Env = append(os.Environ(),
 		// Provide dummy environment variables required to run release.go commands.
-		"TAG_NAME=v0.0.0",    // release tag
+		"TAG_NAME="+tagName,  // release tag
 		"GITHUB_TOKEN=dummy", // github token needed to post release notes
 		"VSCE_PAT=dummy",     // vsce token needed to publish the extension
 		"COMMIT_SHA=4893cd984d190bdf2cd65e11c425b42819ae6f57", // bogus commit SHA used to post release notes
@@ -47,12 +49,12 @@
 		t.Fatalf("failed to run release package: %v", err)
 	}
 	if *flagUpdate {
-		if err := os.WriteFile(filepath.Join("testdata", command+".golden"), output, 0644); err != nil {
+		if err := os.WriteFile(filepath.Join("testdata", command+"-"+tagName+".golden"), output, 0644); err != nil {
 			t.Fatal("failed to write golden file:", err)
 		}
 		return
 	}
-	golden, err := os.ReadFile(filepath.Join("testdata", command+".golden"))
+	golden, err := os.ReadFile(filepath.Join("testdata", command+"-"+tagName+".golden"))
 	if err != nil {
 		t.Fatal("failed to read golden file:", err)
 	}
diff --git a/extension/tools/release/testdata/package-v0.0.0-rc.1.golden b/extension/tools/release/testdata/package-v0.0.0-rc.1.golden
new file mode 100644
index 0000000..961e14a
--- /dev/null
+++ b/extension/tools/release/testdata/package-v0.0.0-rc.1.golden
@@ -0,0 +1,3 @@
+jq -r .version package.json
+cp ../README.md README.md
+npx vsce package -o go-0.0.0-rc.1.vsix --baseContentUrl https://github.com/golang/vscode-go/raw/v0.0.0-rc.1 --baseImagesUrl https://github.com/golang/vscode-go/raw/v0.0.0-rc.1 --no-update-package-json --no-git-tag-version 0.0.0-rc.1
diff --git a/extension/tools/release/testdata/package.golden b/extension/tools/release/testdata/package-v0.0.0.golden
similarity index 100%
rename from extension/tools/release/testdata/package.golden
rename to extension/tools/release/testdata/package-v0.0.0.golden
diff --git a/extension/tools/release/testdata/publish-v0.0.0-rc.1.golden b/extension/tools/release/testdata/publish-v0.0.0-rc.1.golden
new file mode 100644
index 0000000..874492d
--- /dev/null
+++ b/extension/tools/release/testdata/publish-v0.0.0-rc.1.golden
@@ -0,0 +1,4 @@
+jq -r .version package.json
+stat go-0.0.0-rc.1.vsix
+release create --generate-notes --target 4893cd984d190bdf2cd65e11c425b42819ae6f57 --title Release v0.0.0-rc.1 --draft
+gh release create --generate-notes --target 4893cd984d190bdf2cd65e11c425b42819ae6f57 --title Release v0.0.0-rc.1 --draft --prerelease -R github.com/golang/vscode-go v0.0.0-rc.1 go-0.0.0-rc.1.vsix
diff --git a/extension/tools/release/testdata/publish.golden b/extension/tools/release/testdata/publish-v0.0.0.golden
similarity index 100%
rename from extension/tools/release/testdata/publish.golden
rename to extension/tools/release/testdata/publish-v0.0.0.golden