test/gopls: narrow the scope of fake formatTool setting

The extension supports custom formatter. When gopls is used, this
is done by intercepting the formatting request from the middleware
and passing it to the legacy format provider that calls the specified
tool. The "Nonexistent formatter" test is meant to take this
middleware code path and let the legacy format provider fail by
setting `go.formatTool` with a bogus, non-existing tool name.

This setting doesn't need to affect other tests though.
Limit this setting to only the nonexistent formatter, by starting
a new language server instance for each test.

Additionally this fixes bugs:
- wait to show the document to format - the legacy formatter doesn't
  run if there is no visible document. (I don't know why we don't format
  non-visible document but that's how it's written goFormat.ts:24).
- close the open editor after each test.

For golang/vscode-go#1603

Change-Id: I3486b299aa2a2660fb971fed54d3f941be13698d
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/446296
TryBot-Result: kokoro <noreply+kokoro@google.com>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/src/language/legacy/goFormat.ts b/src/language/legacy/goFormat.ts
index a67b130..302394e 100644
--- a/src/language/legacy/goFormat.ts
+++ b/src/language/legacy/goFormat.ts
@@ -56,8 +56,12 @@
 					return Promise.resolve([]);
 				}
 				if (err) {
+					// TODO(hyangah): investigate why this console.log is not visible at all in dev console.
+					// Ideally, this error message should be accessible through one of the output channels.
 					console.log(err);
-					return Promise.reject('Check the console in dev tools to find errors when formatting.');
+					return Promise.reject(
+						`Check the console in dev tools to find errors when formatting with ${formatTool}`
+					);
 				}
 			}
 		);
@@ -70,13 +74,12 @@
 		token: vscode.CancellationToken
 	): Thenable<vscode.TextEdit[]> {
 		const formatCommandBinPath = getBinPath(formatTool);
+		if (!path.isAbsolute(formatCommandBinPath)) {
+			promptForMissingTool(formatTool);
+			return Promise.reject('failed to find tool ' + formatTool);
+		}
 
 		return new Promise<vscode.TextEdit[]>((resolve, reject) => {
-			if (!path.isAbsolute(formatCommandBinPath)) {
-				promptForMissingTool(formatTool);
-				return reject();
-			}
-
 			const env = toolExecutionEnvironment();
 			const cwd = path.dirname(document.fileName);
 			let stdout = '';
diff --git a/test/gopls/extension.test.ts b/test/gopls/extension.test.ts
index 972e8ad..056bb9e 100644
--- a/test/gopls/extension.test.ts
+++ b/test/gopls/extension.test.ts
@@ -92,18 +92,21 @@
 		});
 	}
 
-	public async setup(filePath: string) {
+	// Start the language server with the fakeOutputChannel.
+	public async startGopls(filePath: string, goConfig?: vscode.WorkspaceConfiguration) {
 		// file path to open.
 		this.fakeOutputChannel = new FakeOutputChannel();
 		const pkgLoadingDone = this.onMessageInTrace('Finished loading packages.', 60_000);
 
-		// Start the language server with the fakeOutputChannel.
-		const goConfig = Object.create(getGoConfig(), {
-			useLanguageServer: { value: true },
-			languageServerFlags: { value: ['-rpc.trace'] }, // enable rpc tracing to monitor progress reports
-			formatTool: { value: 'nonexistent' } // to test custom formatters
-		});
-		const cfg: BuildLanguageClientOption = buildLanguageServerConfig(goConfig);
+		if (!goConfig) {
+			goConfig = getGoConfig();
+		}
+		const cfg: BuildLanguageClientOption = buildLanguageServerConfig(
+			Object.create(goConfig, {
+				useLanguageServer: { value: true },
+				languageServerFlags: { value: ['-rpc.trace'] } // enable rpc tracing to monitor progress reports
+			})
+		);
 		cfg.outputChannel = this.fakeOutputChannel; // inject our fake output channel.
 		this.languageClient = await buildLanguageClient({}, cfg);
 		if (!this.languageClient) {
@@ -117,6 +120,7 @@
 
 	public async teardown() {
 		try {
+			await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
 			await this.languageClient?.stop(1_000); // 1s timeout
 		} catch (e) {
 			console.log(`failed to stop gopls within 1sec: ${e}`);
@@ -148,22 +152,24 @@
 	const projectDir = path.join(__dirname, '..', '..', '..');
 	const testdataDir = path.join(projectDir, 'test', 'testdata');
 	const env = new Env();
-
+	const sandbox = sinon.createSandbox();
 	let goVersion: GoVersion;
+
 	suiteSetup(async () => {
-		await env.setup(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		goVersion = await getGoVersion();
 	});
-	suiteTeardown(() => env.teardown());
 
-	this.afterEach(function () {
+	this.afterEach(async function () {
+		await env.teardown();
 		// 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');
+		sandbox.restore();
 	});
 
 	test('HoverProvider', async () => {
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
 		const testCases: [string, vscode.Position, string | null, string | null][] = [
 			// [new vscode.Position(3,3), '/usr/local/go/src/fmt'],
@@ -213,6 +219,7 @@
 	});
 
 	test('Completion middleware', async () => {
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'));
 		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
 		const testCases: [string, vscode.Position, string, vscode.CompletionItemKind][] = [
 			['fmt.P<>', new vscode.Position(19, 6), 'Print', vscode.CompletionItemKind.Function],
@@ -280,14 +287,28 @@
 	});
 
 	test('Nonexistent formatter', async () => {
-		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
-		const result = (await vscode.commands.executeCommand(
-			'vscode.executeFormatDocumentProvider',
-			uri,
-			{} // empty options
-		)) as vscode.TextEdit[];
-		if (result) {
-			assert.fail(`expected no result, got one: ${result}`);
+		const config = require('../../src/config');
+		const goConfig = Object.create(getGoConfig(), {
+			formatTool: { value: 'nonexistent' } // this should make the formatter fail.
+		}) as vscode.WorkspaceConfiguration;
+		sandbox.stub(config, 'getGoConfig').returns(goConfig);
+
+		await env.startGopls(path.resolve(testdataDir, 'gogetdocTestData', 'test.go'), goConfig);
+		const { doc } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
+		await vscode.window.showTextDocument(doc);
+
+		const formatFeature = env.languageClient?.getFeature('textDocument/formatting');
+		const formatter = formatFeature?.getProvider(doc);
+		const tokensrc = new vscode.CancellationTokenSource();
+		try {
+			const result = await formatter?.provideDocumentFormattingEdits(
+				doc,
+				{} as vscode.FormattingOptions,
+				tokensrc.token
+			);
+			assert.fail(`formatter unexpectedly succeeded and returned a result: ${JSON.stringify(result)}`);
+		} catch (e) {
+			assert(`${e}`.includes('errors when formatting with nonexistent'), `${e}`);
 		}
 	});
 });