test: add debug adapter tests

Add basic debug adapter tests to check that basic programs can
run using the vscode-debugadapter-testsupport package.

Updates golang/vscode-go#137

Change-Id: Ida60da545058f229253a27dcc4d66e94f558d113
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/259206
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: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/package-lock.json b/package-lock.json
index 93079f4..f142358 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4711,6 +4711,21 @@
         "vscode-debugprotocol": "1.41.0"
       }
     },
+    "vscode-debugadapter-testsupport": {
+      "version": "1.42.0",
+      "resolved": "https://registry.npmjs.org/vscode-debugadapter-testsupport/-/vscode-debugadapter-testsupport-1.42.0.tgz",
+      "integrity": "sha512-5miGnlsT1VLeXcu04AR+LvD4lwTRlEMAj0nyLgVlMdHzH4RDbX2M2/nXSpRbcsdEHcXoRQlfkAXhM4ZQQ4BUBw==",
+      "requires": {
+        "vscode-debugprotocol": "^1.42.0"
+      },
+      "dependencies": {
+        "vscode-debugprotocol": {
+          "version": "1.42.0",
+          "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.42.0.tgz",
+          "integrity": "sha512-nVsfVCat9FZlOso5SYB1LQQiFGifTyOALpkpJdudDlRXGTpI3mSFiDYXWaoFm7UcfqTOzn1SC7Hqw4d89btT0w=="
+        }
+      }
+    },
     "vscode-debugprotocol": {
       "version": "1.41.0",
       "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.41.0.tgz",
diff --git a/package.json b/package.json
index 81f380f..d4cc1eb 100644
--- a/package.json
+++ b/package.json
@@ -54,6 +54,7 @@
     "semver": "^7.3.2",
     "tree-kill": "file:third_party/tree-kill",
     "vscode-debugadapter": "^1.40.0",
+    "vscode-debugadapter-testsupport": "^1.42.0",
     "vscode-debugprotocol": "^1.40.0",
     "vscode-languageclient": "^6.1.3",
     "web-request": "^1.0.7"
diff --git a/test/fixtures/panic/go.mod b/test/fixtures/panic/go.mod
new file mode 100644
index 0000000..c8c9086
--- /dev/null
+++ b/test/fixtures/panic/go.mod
@@ -0,0 +1 @@
+module github.com/microsoft/vscode-go/gofixtures/panic
diff --git a/test/fixtures/panic/panic.go b/test/fixtures/panic/panic.go
new file mode 100644
index 0000000..6996fb8
--- /dev/null
+++ b/test/fixtures/panic/panic.go
@@ -0,0 +1,5 @@
+package main
+
+func main() {
+	panic("BOOM")
+}
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index 618087f..3b9a7d1 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -2,6 +2,8 @@
 import * as fs from 'fs';
 import * as path from 'path';
 import * as sinon from 'sinon';
+import {DebugClient} from 'vscode-debugadapter-testsupport';
+import {DebugProtocol} from 'vscode-debugprotocol';
 import {
 	Delve,
 	escapeGoModPath,
@@ -9,6 +11,7 @@
 	PackageBuildInfo,
 	RemoteSourcesAndPackages,
 } from '../../src/debugAdapter/goDebug';
+import { GoDebugConfigurationProvider } from '../../src/goDebugConfiguration';
 
 suite('Path Manipulation Tests', () => {
 	test('escapeGoModPath works', () => {
@@ -267,3 +270,160 @@
 		assert.deepEqual(remoteSourcesAndPackages.remotePackagesBuildInfo, [helloPackage, testPackage]);
 	});
 });
+
+// Test suite adapted from:
+// https://github.com/microsoft/vscode-mock-debug/blob/master/src/tests/adapter.test.ts
+suite('Go Debug Adapter', function () {
+	this.timeout(10000);
+
+	const debugConfigProvider = new GoDebugConfigurationProvider();
+
+	const DEBUG_ADAPTER = './dist/debugAdapter.js';
+
+	const PROJECT_ROOT = path.join(__dirname, '../../../');
+	const DATA_ROOT = path.join(PROJECT_ROOT, 'test/fixtures/');
+
+	let dc: DebugClient;
+
+	setup( () => {
+		dc = new DebugClient('node', path.join(PROJECT_ROOT, DEBUG_ADAPTER), 'go');
+		return dc.start();
+	});
+
+	suite('basic', () => {
+
+		test('unknown request should produce error', (done) => {
+			dc.send('illegal_request').then(() => {
+				done(new Error('does not report error on unknown request'));
+			}).catch(() => {
+				done();
+			});
+		});
+	});
+
+	suite('initialize', () => {
+
+		test('should return supported features', () => {
+			return dc.initializeRequest().then((response) => {
+				response.body = response.body || {};
+				assert.strictEqual(response.body.supportsConfigurationDoneRequest, true);
+			});
+		});
+
+		test('should produce error for invalid \'pathFormat\'', (done) => {
+			dc.initializeRequest({
+				adapterID: 'mock',
+				linesStartAt1: true,
+				columnsStartAt1: true,
+				pathFormat: 'url'
+			}).then((response) => {
+				done(new Error('does not report error on invalid \'pathFormat\' attribute'));
+			}).catch((err) => {
+				// error expected
+				done();
+			});
+		});
+	});
+
+	suite('launch', () => {
+		test('should run program to the end', () => {
+
+			const PROGRAM = path.join(DATA_ROOT, 'baseTest');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig),
+				dc.waitForEvent('terminated')
+			]);
+		});
+
+		test('should stop on entry', () => {
+			const PROGRAM = path.join(DATA_ROOT, 'baseTest');
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+				stopOnEntry: true
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+				dc.configurationSequence(),
+				dc.launch(debugConfig),
+				// The debug adapter does not support a stack trace request
+				// when there are no goroutines running. Which is true when it is stopped
+				// on entry. Therefore we would need another method from dc.assertStoppedLocation
+				// to check the debugger is stopped on entry.
+				dc.waitForEvent('stopped').then((event) => {
+					const stevent = event as DebugProtocol.StoppedEvent;
+					assert.strictEqual(stevent.body.reason, 'entry');
+				})
+			]);
+		});
+	});
+
+	suite('setBreakpoints', () => {
+
+		test('should stop on a breakpoint', () => {
+
+			const PROGRAM = path.join(DATA_ROOT, 'baseTest');
+			const FILE = path.join(DATA_ROOT, 'baseTest/test.go');
+
+			const BREAKPOINT_LINE = 11;
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return dc.hitBreakpoint(debugConfig, { path: FILE, line: BREAKPOINT_LINE } );
+		});
+	});
+
+	suite('setExceptionBreakpoints', () => {
+
+		test('should stop on an exception', () => {
+
+			const PROGRAM_WITH_EXCEPTION = path.join(DATA_ROOT, 'panic');
+
+			const config = {
+				name: 'Launch',
+				type: 'go',
+				request: 'launch',
+				mode: 'auto',
+				program: PROGRAM_WITH_EXCEPTION,
+			};
+			const debugConfig = debugConfigProvider.resolveDebugConfiguration(undefined, config);
+
+			return Promise.all([
+
+				dc.waitForEvent('initialized').then(() => {
+					return dc.setExceptionBreakpointsRequest({
+						filters: [ 'all' ]
+					});
+				}).then(() => {
+					return dc.configurationDoneRequest();
+				}),
+
+				dc.launch(debugConfig),
+
+				dc.assertStoppedLocation('panic', {} )
+			]);
+		});
+	});
+});