test: test debug adapter disconnect request

Test disconnecting the debug adapter in different situations, and
confirm that the program is terminated.

Also expands the protection against multiple disconnect requests
to when remoteDebugging is false as well.

Change-Id: Ica1073ff6f4eb47beadacc5e9a87a183288394a8
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/262297
Trust: Suzy Mueller <suzmue@golang.org>
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Polina Sokolova <polina@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/src/debugAdapter/goDebug.ts b/src/debugAdapter/goDebug.ts
index 226f87f..61d7e0d 100644
--- a/src/debugAdapter/goDebug.ts
+++ b/src/debugAdapter/goDebug.ts
@@ -731,6 +731,8 @@
 		const isLocalDebugging: boolean = this.request === 'launch' && !!this.debugProcess;
 
 		return new Promise(async (resolve) => {
+			this.delveConnectionClosed = true;
+
 			// For remote debugging, we want to leave the remote dlv server running,
 			// so instead of killing it via halt+detach, we just close the network connection.
 			// See https://www.github.com/go-delve/delve/issues/1587
@@ -739,7 +741,6 @@
 				const rpcConnection = await this.connection;
 				// tslint:disable-next-line no-any
 				(rpcConnection as any)['conn']['end']();
-				this.delveConnectionClosed = true;
 				return resolve();
 			}
 			const timeoutToken: NodeJS.Timer =
@@ -892,17 +893,21 @@
 		response: DebugProtocol.DisconnectResponse,
 		args: DebugProtocol.DisconnectArguments
 	): Promise<void> {
+		// There is a chance that a second disconnectRequest can come through
+		// if users click detach multiple times. In that case, we want to
+		// guard against talking to the closed Delve connection.
+		// Note: this does not completely guard against users attempting to
+		// disconnect multiple times when a disconnect request is still running.
+		// The order of the execution may results in strange states that don't allow
+		// the delve connection to fully disconnect.
+		if (this.delve.delveConnectionClosed) {
+			log(`Skip disconnectRequestHelper as Delve's connection is already closed.`);
+			return;
+		}
+
 		// For remote process, we have to issue a continue request
 		// before disconnecting.
 		if (this.delve.isRemoteDebugging) {
-			// There is a chance that a second disconnectRequest can come through
-			// if users click detach multiple times. In that case, we want to
-			// guard against talking to the closed Delve connection.
-			if (this.delve.delveConnectionClosed) {
-				log(`Skip disconnectRequestHelper as Delve's connection is already closed.`);
-				return;
-			}
-
 			if (!(await this.isDebuggeeRunning())) {
 				log(`Issuing a continue command before closing Delve's connection as the debuggee is not running.`);
 				this.continue();
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index 8f1fb67..186bdcf 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -889,6 +889,10 @@
 	});
 
 	suite('disconnect', () => {
+		// The teardown code for the Go Debug Adapter test suite issues a disconnectRequest.
+		// In order for these tests to pass, the debug adapter must not fail if a
+		// disconnectRequest is sent after it has already disconnected.
+
 		test('disconnect should work for remote attach', async () => {
 			this.timeout(30_000);
 			const server = await getPort();
@@ -922,5 +926,208 @@
 			await killProcessTree(remoteProgram);
 			await new Promise((resolve) => setTimeout(resolve, 2_000));
 		});
+
+		test('should disconnect while continuing on entry', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: false
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect with multiple disconnectRequests', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: false
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			await Promise.all([
+				dc.disconnectRequest({restart: false}).then(() =>
+					dc.disconnectRequest({restart: false})
+				),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect after continue', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			const continueResponse = await dc.continueRequest({ threadId: 1 });
+			assert.ok(continueResponse.success);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect while nexting', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'sleep');
+			const FILE = path.join(DATA_ROOT, 'sleep', 'sleep.go');
+			const BREAKPOINT_LINE = 11;
+			const location = getBreakpointLocation(FILE, BREAKPOINT_LINE);
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: false
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, location);
+
+			const nextResponse = await dc.nextRequest({ threadId: 1 });
+			assert.ok(nextResponse.success);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect while paused on pause', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			const pauseResponse = await dc.pauseRequest({threadId: 1});
+			assert.ok(pauseResponse.success);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated'),
+			]);
+		});
+
+		test('should disconnect while paused on breakpoint', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+			const FILE = path.join(PROGRAM, 'loop.go');
+			const BREAKPOINT_LINE = 5;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await dc.hitBreakpoint(debugConfig, { path: FILE, line: BREAKPOINT_LINE } );
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect while paused on entry', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should disconnect while paused on next', async () => {
+			const PROGRAM = path.join(DATA_ROOT, 'loop');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			await Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig)
+			]);
+
+			const nextResponse = await dc.nextRequest({ threadId: 1 });
+			assert.ok(nextResponse.success);
+
+			return Promise.all([
+				dc.disconnectRequest({restart: false}),
+				dc.waitForEvent('terminated')
+			]);
+		});
 	});
 });
diff --git a/test/testdata/loop/go.mod b/test/testdata/loop/go.mod
new file mode 100644
index 0000000..9d7c7a7
--- /dev/null
+++ b/test/testdata/loop/go.mod
@@ -0,0 +1,3 @@
+module github.com/microsoft/vscode-go/gofixtures/loop
+
+go 1.14
diff --git a/test/testdata/loop/loop.go b/test/testdata/loop/loop.go
new file mode 100644
index 0000000..9740987
--- /dev/null
+++ b/test/testdata/loop/loop.go
@@ -0,0 +1,7 @@
+package main
+
+func main() {
+	for {
+		print("Hello")
+	}
+}
diff --git a/test/testdata/sleep/go.mod b/test/testdata/sleep/go.mod
new file mode 100644
index 0000000..7b48880
--- /dev/null
+++ b/test/testdata/sleep/go.mod
@@ -0,0 +1,3 @@
+module github.com/microsoft/vscode-go/gofixtures/sleep
+
+go 1.14
diff --git a/test/testdata/sleep/sleep.go b/test/testdata/sleep/sleep.go
new file mode 100644
index 0000000..fa3ea47
--- /dev/null
+++ b/test/testdata/sleep/sleep.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for i := 0; i < 3; i++ {
+		fmt.Println("Hello!")
+		time.Sleep(2 * time.Second)
+		fmt.Println("Goodbye!")
+	}
+}