test/gopls: change test environment setup to use single file edit

When we wrote the first gopls integration test, gopls required
vscode to open the workspace folder while vscode code does not
support dynamic workspace folder registration in test mode. As a
result, we ended up having a fixed workspace folder as a test fixture,
starting the test code instance with the folder, and copying
necessary files to the folder. There were so many moving parts
and this created race conditions and caused slow test run.

Since v0.4.x, gopls starts to support single file edit and automatically
constructs an internal workspace by walking the directory tree
around the open file. This CL utilizes this new capability.
In each suite, we start testing by starting a new gopls, opening
a single file, and waiting for the gopls to finish the initial package
loading.

This CL introduces Env.onPatternInTrace, which watches the
fake trace output channel, and emits an event when a registered
pattern is observed. This is a hack to wait for the gopls's internal
state to reach to a desirable state - ideally, we want to intercept
all the progress report messages like gopls' regression tests
once it is supported from the language client middleware.
https://github.com/microsoft/vscode-languageserver-node/issues/671

We also identified subtle issues in the existing test setup.

Issue 1: when the code for testing starts (using `vscode-test`'s
`runTests`) we pass the extension development path. It seems like
the vscode instance uses the `main` program specified in `package.json`
and activates it even without us asking. As a result, when we run
tests and call 'activate' again, multiple hover/completion providers
are registered, and vscode returns results from legacy and gopls-based
providers. For example, the completion middleware test was observing
entries from gopls, and goCompletionItemProvider that uses gocode.

We address this issue here by introducing the VSCODE_GO_IN_TEST
environment variable. If it is set, activate will return immediately.
So, tests can control when to register what, and how.

We need this setting in both `launch.json` and `runTest.ts` that's
invoked in CI (`npm run test`)

Issue 2: when the code for testing needs to call `activate`, we
got the extension instance by using `vscode.extensions.getExtension`
and called its `activate`. This was because there is no easy way
to supply sufficiently complete vscode's ExtensionContext.
It turned out, the extension instance vscode.extensions.getExtension
returns is the one built with the `main` program specified in
our `package.json` - that is the webpack'ed one in `dist/goMain.js`.
On the other hand, our debugging depends on pre-webpack versions
in `out/*`. This caused confusion and made debugging near impossible
(breakpoints we set on pre-webpack versions will not be hit because
we are running a different version of extension)!

We don't know if there is a way to teach `vscode-test` to use pre-webpack
version. Maybe this is our misconfiguration in our launch.json and
package.json. For now, we work around this issue by not calling
`activate`. Instead, in this gopls test, we call `buildLanguageClient`
directly. This required some refactoring work in goLanguageServer.ts.

Issue 3: sinon is cool, but stubbing vscode API for channel creation
is too much. While we refactor buildLanguageClient, we made changes
to let the caller supply the output channels.

Issue 4: as `vscode-test` starts the test vscode instance, it also
activates the registered snippets and it interferes with our gopls
completion middleware tests. In test, now we explicitly filter out
the snippet entries.

Issue 5: for some reason, the first entry in the completion middleware
test that expects 'Print' item, the filter text is not set. It can be
a bug, or working as intended (the first item has label === filterText).
Gopls is providing the expected entry. Workaround this issue by inspecting
the label field too.

Updates golang/vscode-go#655
Updates golang/vscode-go#832

Change-Id: Ic7088fd551329d1c8f78078ccb24a5f529eec72a
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/266418
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 4f23b75..3ec6c82 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -64,10 +64,14 @@
 				"--timeout",
 				"999999"
 			],
+			"env": {
+				"VSCODE_GO_IN_TEST": "1"  // Disable code that shouldn't be used in test
+			},
 			"stopOnEntry": false,
 			"sourceMaps": true,
 			"smartStep": true,
 			"outFiles": [
+				"${workspaceFolder}/out/**/*.js",
 				"${workspaceFolder}/out/test/**/*.js"
 			],
 			"preLaunchTask": "npm: watch"
@@ -83,11 +87,14 @@
 				"--extensionDevelopmentPath=${workspaceFolder}",
 				"--extensionTestsPath=${workspaceFolder}/out/test/gopls/index",
 				"--timeout", "999999",
-				"${workspaceFolder}/test/gopls/testfixtures/src/workspace"  // gopls requires a workspace to work with.
 			],
+			"env": {
+				"VSCODE_GO_IN_TEST": "1"  // Disable code that shouldn't be used in test
+			},
 			"stopOnEntry": false,
 			"sourceMaps": true,
 			"outFiles": [
+				"${workspaceFolder}/out/**/*.js",
 				"${workspaceFolder}/out/test/**/*.js"
 			],
 			"preLaunchTask": "npm: watch",
diff --git a/src/goCheck.ts b/src/goCheck.ts
index 597e018..327a5f8 100644
--- a/src/goCheck.ts
+++ b/src/goCheck.ts
@@ -59,7 +59,7 @@
 
 	// If a user has enabled diagnostics via a language server,
 	// then we disable running build or vet to avoid duplicate errors and warnings.
-	const lspConfig = buildLanguageServerConfig();
+	const lspConfig = buildLanguageServerConfig(goConfig);
 	const disableBuildAndVet = lspConfig.enabled && lspConfig.features.diagnostics;
 
 	let testPromise: Thenable<boolean>;
diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts
index 59974cf..2e3a06a 100644
--- a/src/goInstallTools.ts
+++ b/src/goInstallTools.ts
@@ -466,6 +466,11 @@
 		});
 	}
 
+	const goConfig = getGoConfig();
+	if (!goConfig['useLanguageServer']) {
+		return;
+	}
+
 	const usingSourceGraph = getToolFromToolPath(getLanguageServerToolPath()) === 'go-langserver';
 	if (usingSourceGraph && goVersion.gt('1.10')) {
 		const promptMsg =
@@ -476,7 +481,6 @@
 		if (selected === installLabel) {
 			await installTools([getTool('gopls')], goVersion);
 		} else if (selected === disableLabel) {
-			const goConfig = getGoConfig();
 			const inspectLanguageServerSetting = goConfig.inspect('useLanguageServer');
 			if (inspectLanguageServerSetting.globalValue === true) {
 				goConfig.update('useLanguageServer', false, vscode.ConfigurationTarget.Global);
diff --git a/src/goLanguageServer.ts b/src/goLanguageServer.ts
index 9d0dcf7..30256c9 100644
--- a/src/goLanguageServer.ts
+++ b/src/goLanguageServer.ts
@@ -58,7 +58,7 @@
 import { getBinPath, getCurrentGoPath, getGoConfig, getGoplsConfig, getWorkspaceFolderPath } from './util';
 import { getToolFromToolPath } from './utils/pathUtils';
 
-interface LanguageServerConfig {
+export interface LanguageServerConfig {
 	serverName: string;
 	path: string;
 	version: string;
@@ -102,7 +102,7 @@
 // startLanguageServerWithFallback starts the language server, if enabled,
 // or falls back to the default language providers.
 export async function startLanguageServerWithFallback(ctx: vscode.ExtensionContext, activation: boolean) {
-	const cfg = buildLanguageServerConfig();
+	const cfg = buildLanguageServerConfig(getGoConfig());
 
 	// If the language server is gopls, we enable a few additional features.
 	// These include prompting for updates and surveys.
@@ -134,7 +134,7 @@
 	const update = async () => {
 		setTimeout(update, timeDay);
 
-		const cfg = buildLanguageServerConfig();
+		const cfg = buildLanguageServerConfig(getGoConfig());
 		if (!cfg.enabled) {
 			return;
 		}
@@ -146,7 +146,7 @@
 	const survey = async () => {
 		setTimeout(survey, timeDay);
 
-		const cfg = buildLanguageServerConfig();
+		const cfg = buildLanguageServerConfig(getGoConfig());
 		if (!goplsSurveyOn || !cfg.enabled) {
 			return;
 		}
@@ -176,7 +176,7 @@
 		// Track the latest config used to start the language server,
 		// and rebuild the language client.
 		latestConfig = config;
-		languageClient = await buildLanguageClient(config);
+		languageClient = await buildLanguageClient(buildLanguageClientOption(config));
 		crashCount = 0;
 	}
 
@@ -206,21 +206,38 @@
 	return true;
 }
 
-async function buildLanguageClient(cfg: LanguageServerConfig): Promise<LanguageClient> {
-	// Reuse the same output channel for each instance of the server.
-	if (cfg.enabled) {
-		if (!serverOutputChannel) {
-			serverOutputChannel = vscode.window.createOutputChannel(cfg.serverName + ' (server)');
+export interface BuildLanguageClientOption extends LanguageServerConfig {
+	outputChannel?: vscode.OutputChannel;
+	traceOutputChannel?: vscode.OutputChannel;
+}
+
+// buildLanguageClientOption returns the default, extra configuration
+// used in building a new LanguageClient instance. Options specified
+// in LanguageServerConfig
+function buildLanguageClientOption(cfg: LanguageServerConfig): BuildLanguageClientOption {
+		// Reuse the same output channel for each instance of the server.
+		if (cfg.enabled) {
+			if (!serverOutputChannel) {
+				serverOutputChannel = vscode.window.createOutputChannel(cfg.serverName + ' (server)');
+			}
+			if (!serverTraceChannel) {
+				serverTraceChannel = vscode.window.createOutputChannel(cfg.serverName);
+			}
 		}
-		if (!serverTraceChannel) {
-			serverTraceChannel = vscode.window.createOutputChannel(cfg.serverName);
-		}
-	}
+		return Object.assign({
+			outputChannel: serverOutputChannel,
+			traceOutputChannel: serverTraceChannel
+		}, cfg);
+}
+
+// buildLanguageClient returns a language client built using the given language server config.
+// The returned language client need to be started before use.
+export async function buildLanguageClient(cfg: BuildLanguageClientOption): Promise<LanguageClient> {
 	let goplsWorkspaceConfig = getGoplsConfig();
 	goplsWorkspaceConfig = await adjustGoplsWorkspaceConfiguration(cfg, goplsWorkspaceConfig);
 	const c = new LanguageClient(
 		'go',  // id
-		cfg.serverName,  // name
+		cfg.serverName,  // name e.g. gopls
 		{
 			command: cfg.path,
 			args: ['-mode=stdio', ...cfg.flags],
@@ -235,8 +252,8 @@
 					(uri.scheme ? uri : uri.with({ scheme: 'file' })).toString(),
 				protocol2Code: (uri: string) => vscode.Uri.parse(uri)
 			},
-			outputChannel: serverOutputChannel,
-			traceOutputChannel: serverTraceChannel,
+			outputChannel: cfg.outputChannel,
+			traceOutputChannel: cfg.traceOutputChannel,
 			revealOutputChannelOn: RevealOutputChannelOn.Never,
 			initializationFailedHandler: (error: WebRequest.ResponseError<InitializeError>): boolean => {
 				vscode.window.showErrorMessage(
@@ -550,8 +567,8 @@
 	}
 }
 
-export function buildLanguageServerConfig(): LanguageServerConfig {
-	const goConfig = getGoConfig();
+export function buildLanguageServerConfig(goConfig: vscode.WorkspaceConfiguration): LanguageServerConfig {
+
 	const cfg: LanguageServerConfig = {
 		serverName: '',
 		path: '',
@@ -604,9 +621,6 @@
  */
 export function getLanguageServerToolPath(): string {
 	const goConfig = getGoConfig();
-	if (!goConfig['useLanguageServer']) {
-		return;
-	}
 	// Check that all workspace folders are configured with the same GOPATH.
 	if (!allFoldersHaveSameGopath()) {
 		vscode.window.showInformationMessage(
diff --git a/src/goMain.ts b/src/goMain.ts
index 78099ae..7c370c5 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -83,6 +83,9 @@
 export let restartLanguageServer = () => { return; };
 
 export function activate(ctx: vscode.ExtensionContext) {
+	if (process.env['VSCODE_GO_IN_TEST'] === '1') {  // Make sure this does not run when running in test.
+		return;
+	}
 	const cfg = getGoConfig();
 	setLogConfig(cfg['logging']);
 
diff --git a/src/goStatus.ts b/src/goStatus.ts
index 08ec245..bcd4bbb 100644
--- a/src/goStatus.ts
+++ b/src/goStatus.ts
@@ -12,7 +12,7 @@
 import { buildLanguageServerConfig, getLocalGoplsVersion, serverOutputChannel } from './goLanguageServer';
 import { isGoFile } from './goMode';
 import { getModFolderPath, isModSupported } from './goModules';
-import { getGoVersion } from './util';
+import { getGoConfig, getGoVersion } from './util';
 
 export let outputChannel = vscode.window.createOutputChannel('Go');
 
@@ -47,7 +47,7 @@
 	];
 
 	// Get the gopls configuration
-	const cfg = buildLanguageServerConfig();
+	const cfg = buildLanguageServerConfig(getGoConfig());
 	if (cfg.serverName === 'gopls') {
 		const goplsVersion = await getLocalGoplsVersion(cfg);
 		options.push({label: `${languageServerIcon}Open 'gopls' trace`, description: `${goplsVersion}`});
@@ -101,7 +101,7 @@
 	// Add an icon to indicate that the 'gopls' server is running.
 	// Assume if it is configured it is already running, since the
 	// icon will be updated on an attempt to start.
-	const cfg = buildLanguageServerConfig();
+	const cfg = buildLanguageServerConfig(getGoConfig());
 	updateLanguageServerIconGoStatusBar(true, cfg.serverName);
 
 	showGoStatusBar();
diff --git a/test/gopls/extension.test.ts b/test/gopls/extension.test.ts
index 4df045f..7e82399 100644
--- a/test/gopls/extension.test.ts
+++ b/test/gopls/extension.test.ts
@@ -4,11 +4,13 @@
  *--------------------------------------------------------*/
 import * as assert from 'assert';
 import cp = require('child_process');
-import * as fs from 'fs-extra';
+import { EventEmitter } from 'events';
 import * as path from 'path';
 import sinon = require('sinon');
 import * as vscode from 'vscode';
-import { extensionId } from '../../src/const';
+import { LanguageClient } from 'vscode-languageclient/node';
+import { buildLanguageClient, BuildLanguageClientOption, buildLanguageServerConfig } from '../../src/goLanguageServer';
+import { getGoConfig } from '../../src/util';
 
 // FakeOutputChannel is a fake output channel used to buffer
 // the output of the tested language client in an in-memory
@@ -21,6 +23,16 @@
 
 	private buf = [] as string[];
 
+	private eventEmitter = new EventEmitter();
+	private registeredPatterns = new Set<string>();
+	public onPattern(msg: string, listener: () => void) {
+		this.registeredPatterns.add(msg);
+		this.eventEmitter.once(msg, () => {
+			this.registeredPatterns.delete(msg);
+			listener();
+		});
+	}
+
 	public append = (v: string) => this.enqueue(v);
 	public appendLine = (v: string) => this.enqueue(v);
 	public clear = () => { this.buf = []; };
@@ -29,98 +41,75 @@
 	}
 
 	private enqueue = (v: string) => {
+		this.registeredPatterns?.forEach((p) => {
+			if (v.includes(p)) {
+				this.eventEmitter.emit(p);
+			}
+		});
+
 		if (this.buf.length > 1024) { this.buf.shift(); }
 		this.buf.push(v.trim());
 	}
 }
 
-// Env is a collection of test related variables
-// that define the test environment such as vscode workspace.
+// Env is a collection of test-related variables and lsp client.
+// Currently, this works only in module-aware mode.
 class Env {
-
-	// Currently gopls requires a workspace and does not work in a single-file mode.
-	// Code in test environment does not support dynamically adding folders.
-	// tslint:disable-next-line:max-line-length
-	// https://github.com/microsoft/vscode/blob/890f62dfd9f3e70198931f788c5c332b3e8b7ad7/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts#L281
-	//
-	// So, when we start the gopls tests, we start the test extension host with a
-	// dummy workspace, ${projectDir}/test/gopls/testdata/src/workspace
-	// (see test/runTest.ts and launch.json).
-	// Then copy necessary files to the workspace using Env.reset() from the
-	// fixturesRoot directory.
-	public workspaceDir: string;
-	public fixturesRoot: string;
-
-	public extension: vscode.Extension<any>;
-
+	public languageClient?: LanguageClient;
 	private fakeOutputChannel: FakeOutputChannel;
-
-	constructor(projectDir: string) {
-		if (!projectDir) {
-			assert.fail('project directory cannot be determined');
-		}
-		this.workspaceDir = path.resolve(projectDir, 'test/gopls/testdata/src/workspace');
-		this.fixturesRoot = path.resolve(projectDir, 'test/testdata');
-		this.extension = vscode.extensions.getExtension(extensionId);
-		this.fakeOutputChannel = new FakeOutputChannel();
-
-		// Ensure the vscode extension host is configured as expected.
-		const workspaceFolder = path.resolve(vscode.workspace.workspaceFolders[0].uri.fsPath);
-		if (this.workspaceDir !== workspaceFolder) {
-			assert.fail(`specified workspaceDir: ${this.workspaceDir} does not match the workspace folder: ${workspaceFolder}`);
-		}
-	}
+	private disposables = [] as { dispose(): any }[];
 
 	public flushTrace(print: boolean) {
 		if (print) {
 			console.log(this.fakeOutputChannel.toString());
-			this.fakeOutputChannel.clear();
 		}
 	}
 
-	public async setup() {
-		// stub the language server's output channel to intercept the trace.
-		sinon.stub(vscode.window, 'createOutputChannel')
-			.callThrough().withArgs('gopls (server)').returns(this.fakeOutputChannel);
-
-		await this.reset();
-		await this.extension.activate();
-		await sleep(2000);  // allow the language server to start.
-		// TODO(hyangah): find a better way to check the language server's status.
-		// I thought I'd check the languageClient.onReady(),
-		// but couldn't make it working yet.
-	}
-
-	public teardown() {
-		sinon.restore();
-	}
-
-	public async reset(fixtureDirName?: string) {  // name of the fixtures subdirectory to use.
-		try {
-			// clean everything except the .gitignore file
-			// needed to keep the empty directory in vcs.
-			await fs.readdir(this.workspaceDir).then((files) => {
-				return Promise.all(
-					files.filter((filename) => filename !== '.gitignore' && filename !== '.vscode').map((file) => {
-						fs.remove(path.resolve(this.workspaceDir, file));
-					}));
+	// This is a hack to check the progress of package loading.
+	// TODO(hyangah): use progress message middleware hook instead
+	// once it becomes available.
+	public onMessageInTrace(msg: string, timeoutMS: number): Promise<void> {
+		return new Promise((resolve, reject) => {
+			const timeout = setTimeout(() => {
+				this.flushTrace(true);
+				reject(`Timed out while waiting for '${msg}'`);
+			}, timeoutMS);
+			this.fakeOutputChannel.onPattern(msg, () => {
+				clearTimeout(timeout);
+				resolve();
 			});
-
-			if (!fixtureDirName) {
-				return;
-			}
-			const src = path.resolve(this.fixturesRoot, fixtureDirName);
-			const dst = this.workspaceDir;
-			await fs.copy(src, dst, { recursive: true });
-		} catch (err) {
-			assert.fail(err);
-		}
+		});
 	}
 
-	// openDoc opens the file in the workspace with the given path (paths
-	// are the path elements of a file).
+	public async setup(filePath: string) {  // 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
+		});
+		const cfg: BuildLanguageClientOption = buildLanguageServerConfig(goConfig);
+		cfg.outputChannel = this.fakeOutputChannel;  // inject our fake output channel.
+		this.languageClient = await buildLanguageClient(cfg);
+		this.disposables.push(this.languageClient.start());
+
+		await this.languageClient.onReady();
+		await this.openDoc(filePath);
+		await pkgLoadingDone;
+	}
+
+	public async teardown() {
+		await this.languageClient?.stop();
+		for (const d of this.disposables) {
+			d.dispose();
+		}
+		this.languageClient = undefined;
+	}
+
 	public async openDoc(...paths: string[]) {
-		const uri = vscode.Uri.file(path.resolve(this.workspaceDir, ...paths));
+		const uri = vscode.Uri.file(path.resolve(...paths));
 		const doc = await vscode.workspace.openTextDocument(uri);
 		return { uri, doc };
 	}
@@ -133,12 +122,11 @@
 suite('Go Extension Tests With Gopls', function () {
 	this.timeout(300000);
 	const projectDir = path.join(__dirname, '..', '..', '..');
-	const env = new Env(projectDir);
+	const testdataDir = path.join(projectDir, 'test', 'testdata');
+	const env = new Env();
 
-	suiteSetup(async () => {
-		await env.setup();
-	});
-	suiteTeardown(async () => { await env.reset(); });
+	suiteSetup(async () => await env.setup(path.resolve(testdataDir, 'gogetdocTestData', 'test.go')));
+	suiteTeardown(() => env.teardown());
 
 	this.afterEach(function () {
 		// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
@@ -148,11 +136,7 @@
 	});
 
 	test('HoverProvider', async () => {
-		await env.reset('gogetdocTestData');
-		const { uri, doc } = await env.openDoc('test.go');
-
-		// TODO(hyangah): find a way to wait for the language server to complete processing.
-
+		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'],
 			['keyword', new vscode.Position(0, 3), null, null], // keyword
@@ -186,8 +170,7 @@
 	});
 
 	test('Completion middleware', async () => {
-		await env.reset('gogetdocTestData');
-		const { uri } = await env.openDoc('test.go');
+		const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
 		const testCases: [string, vscode.Position, string][] = [
 			['fmt.P<>', new vscode.Position(19, 6), 'Print'],
 		];
@@ -213,14 +196,18 @@
 			if (!list.isIncomplete) {
 				assert.fail(`gopls should provide an incomplete list by default`);
 			}
-			// TODO(rstambler): For some reason, the filter text gets deleted
-			// from the first item. I can't reproduce this outside of the test
-			// suite.
-			for (let i = 1; i < list.items.length; i++) {
-				const item = list.items[i];
-				assert.equal(item.filterText, wantFilterText, `${uri}:${name} failed, unexpected filter text (got ${item.filterText}, want ${wantFilterText})`);
-			}
+
+			// vscode.executeCompletionItemProvider will return results from all
+			// registered completion item providers, not only gopls but also snippets.
+			// Alternative is to directly query the language client, but that will
+			// prevent us from detecting problems caused by issues between the language
+			// client library and the vscode.
 			for (const item of list.items) {
+				if (item.kind === vscode.CompletionItemKind.Snippet) { continue; }  // gopls does not supply Snippet yet.
+				assert.strictEqual(item.filterText ?? item.label, wantFilterText,
+					`${uri}:${name} failed, unexpected filter text ` +
+					`(got ${item.filterText ?? item.label}, want ${wantFilterText})\n` +
+					`${JSON.stringify(item, null, 2)}`);
 				if (item.kind === vscode.CompletionItemKind.Method || item.kind === vscode.CompletionItemKind.Function) {
 					assert.ok(item.command, `${uri}:${name}: expected command associated with ${item.label}, found none`);
 				}
diff --git a/test/integration/extension.test.ts b/test/integration/extension.test.ts
index b7ca763..66dd9c3 100644
--- a/test/integration/extension.test.ts
+++ b/test/integration/extension.test.ts
@@ -429,7 +429,7 @@
 		];
 		// If a user has enabled diagnostics via a language server,
 		// then we disable running build or vet to avoid duplicate errors and warnings.
-		const lspConfig = buildLanguageServerConfig();
+		const lspConfig = buildLanguageServerConfig(getGoConfig());
 		const expectedBuildVetErrors = lspConfig.enabled ? [] : [{ line: 11, severity: 'error', msg: 'undefined: prin' }];
 
 		const expected = [...expectedLintErrors, ...expectedBuildVetErrors];
@@ -1370,7 +1370,7 @@
 	});
 
 	test('Build Tags checking', async () => {
-		const goplsConfig = buildLanguageServerConfig();
+		const goplsConfig = buildLanguageServerConfig(getGoConfig());
 		if (goplsConfig.enabled) {
 			// Skip this test if gopls is enabled. Build/Vet checks this test depend on are
 			// disabled when the language server is enabled, and gopls is not handling tags yet.
diff --git a/test/runTest.ts b/test/runTest.ts
index 22dc780..c047691 100644
--- a/test/runTest.ts
+++ b/test/runTest.ts
@@ -2,6 +2,9 @@
 import { runTests } from 'vscode-test';
 
 async function main() {
+	// We are in test mode.
+	process.env['VSCODE_GO_IN_TEST'] = '1';
+
 	// The folder containing the Extension Manifest package.json
 	// Passed to `--extensionDevelopmentPath`
 	const extensionDevelopmentPath = path.resolve(__dirname, '../../');
@@ -26,20 +29,14 @@
 
 	// Integration tests using gopls.
 	try {
-		// Currently gopls requires a workspace. Code in test environment does not support
-		// dynamically adding folders.
+		// Note: Code in test environment does not support dynamically adding folders.
 		// tslint:disable-next-line:max-line-length
 		// https://github.com/microsoft/vscode/blob/890f62dfd9f3e70198931f788c5c332b3e8b7ad7/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts#L281
-		// So, we start the test extension host with a dummy workspace (test/gopls/testdata/src/workspace)
-		// and copy necessary files to the workspace.
-		const ws = path.resolve(extensionDevelopmentPath, 'test/gopls/testdata/src/workspace');
-
 		await runTests({
 			extensionDevelopmentPath,
 			extensionTestsPath: path.resolve(__dirname, './gopls/index'),
 			launchArgs: [
 				'--disable-extensions',  // disable all other extensions
-				ws  // dummy workspace to start with
 			],
 		});
 	} catch (err) {