test/integration/goDebug: test to change to correct goroutine when stepping

The debugger needs to switch to the correct goroutine if the threadId
in the request does not match the current goroutine. This change
checks the current state to decide if it should switch goroutines,
and then does so. This adds a test for 'next' and 'step in'
requests that switch goroutines.

Updates golang/vscode-go#118

Change-Id: I553ebbed6de1001e0e7da756278fcc717884f946
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/300769
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index 33eba6c..420d28e 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -1525,6 +1525,126 @@
 		});
 	});
 
+	suite('switch goroutine', () => {
+		async function runSwitchGoroutineTest(stepFunction: string) {
+			const PROGRAM = path.join(DATA_ROOT, 'goroutineTest');
+			const FILE = path.join(PROGRAM, 'main.go');
+			const BREAKPOINT_LINE = 14;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'debug',
+				program: PROGRAM
+			};
+			const debugConfig = await initializeDebugConfig(config);
+			await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
+			// Clear breakpoints to make sure they do not interrupt the stepping.
+			const breakpointsResult = await dc.setBreakpointsRequest({
+				source: { path: FILE },
+				breakpoints: []
+			});
+			assert.ok(breakpointsResult.success);
+
+			const threadsResponse = await dc.threadsRequest();
+			assert.ok(threadsResponse.success);
+			const run1Goroutine = threadsResponse.body.threads.find((val) => val.name.indexOf('main.run1') >= 0);
+			const run2Goroutine = threadsResponse.body.threads.find((val) => val.name.indexOf('main.run2') >= 0);
+
+			// runStepFunction runs the necessary step function and resolves if it succeeded.
+			async function runStepFunction(
+				args: { threadId: number },
+				resolve: (value: void | PromiseLike<void>) => void,
+				reject: (reason?: any) => void
+			) {
+				const callback = (resp: any) => {
+					assert.ok(resp.success);
+					resolve();
+				};
+				switch (stepFunction) {
+					case 'next':
+						callback(await dc.nextRequest(args));
+						break;
+					case 'step in':
+						callback(await dc.stepInRequest(args));
+						break;
+					case 'step out':
+						// TODO(suzmue): write a test for step out.
+						reject(new Error('step out will never complete on this program'));
+						break;
+					default:
+						reject(new Error(`not a valid step function ${stepFunction}`));
+				}
+			}
+
+			// The program is currently stopped on the goroutine in main.run2.
+			// Test switching go routines by stepping in:
+			//   1. main.run2
+			//   2. main.run1 (switch routine)
+			//   3. main.run1
+			//   4. main.run2 (switch routine)
+
+			// Next on the goroutine in main.run2
+			await Promise.all([
+				new Promise<void>((resolve, reject) => {
+					const args = { threadId: run2Goroutine.id };
+					return runStepFunction(args, resolve, reject);
+				}),
+				dc.waitForEvent('stopped').then((event) => {
+					assert.strictEqual(event.body.reason, 'step');
+					assert.strictEqual(event.body.threadId, run2Goroutine.id);
+				})
+			]);
+
+			// Next on the goroutine in main.run1
+			await Promise.all([
+				new Promise<void>((resolve, reject) => {
+					const args = { threadId: run1Goroutine.id };
+					return runStepFunction(args, resolve, reject);
+				}),
+				dc.waitForEvent('stopped').then((event) => {
+					assert.strictEqual(event.body.reason, 'step');
+					assert.strictEqual(event.body.threadId, run1Goroutine.id);
+				})
+			]);
+
+			// Next on the goroutine in main.run1
+			await Promise.all([
+				new Promise<void>((resolve, reject) => {
+					const args = { threadId: run1Goroutine.id };
+					return runStepFunction(args, resolve, reject);
+				}),
+				dc.waitForEvent('stopped').then((event) => {
+					assert.strictEqual(event.body.reason, 'step');
+					assert.strictEqual(event.body.threadId, run1Goroutine.id);
+				})
+			]);
+
+			// Next on the goroutine in main.run2
+			await Promise.all([
+				new Promise<void>((resolve, reject) => {
+					const args = { threadId: run2Goroutine.id };
+					return runStepFunction(args, resolve, reject);
+				}),
+				dc.waitForEvent('stopped').then((event) => {
+					assert.strictEqual(event.body.reason, 'step');
+					assert.strictEqual(event.body.threadId, run2Goroutine.id);
+				})
+			]);
+		}
+
+		test.skip('next', async () => {
+			// neither debug adapter implements this behavior
+			await runSwitchGoroutineTest('next');
+		});
+
+		test.skip('step in', async () => {
+			// neither debug adapter implements this behavior
+			await runSwitchGoroutineTest('step in');
+		});
+	});
+
 	suite('substitute path', () => {
 		// TODO(suzmue): add unit tests for substitutePath.
 		let tmpDir: string;
diff --git a/test/testdata/goroutineTest/go.mod b/test/testdata/goroutineTest/go.mod
new file mode 100644
index 0000000..6f52d42
--- /dev/null
+++ b/test/testdata/goroutineTest/go.mod
@@ -0,0 +1,3 @@
+module github.com/golang/vscode-go/gofixtures/goroutineTest
+
+go 1.16
diff --git a/test/testdata/goroutineTest/main.go b/test/testdata/goroutineTest/main.go
new file mode 100644
index 0000000..38055ed
--- /dev/null
+++ b/test/testdata/goroutineTest/main.go
@@ -0,0 +1,24 @@
+package main
+
+func run1() {
+	x := 0
+	for {
+		x++
+		x *= 4
+	}
+}
+
+func run2() {
+	x := 0
+	for {
+		x++
+		x *= 4
+	}
+}
+
+func main() {
+	go run1()
+	go run2()
+	for {
+	}
+}