test/integration: use DelveDAPOutputAdapter in dlv-dap mode testing

Previously, dlv-dap mode tests directly spawned a dlv dap server
and made the debug client directly connect to it. This change makes
the tests use DelveDAPOutputAdapter the extension uses to spawn
a dlv dap server and proxies DAP requests/responses and its
stdout/stderr. This allows to see the debugee's output and test
the most fragile, complex part in the new adapter integration.

The test support library only works debug adapters that implements
DAP protocol server (JSON encoding/decoding over a network socket
or stdin/out). DelveDAPDebugAdapterOnSocket is an extension of
DelveDAPOutputAdapter that adds JSON protocol encoding/decoding
over a network socket. Tests now create a DelveDAPDebugAdapterOnSocket
and connect the DebugClient with its network port.

DelveDAPDebugAdapterOnSocket provides another vantage point to
trace DAP protocol exchange, so we use that to capture the traffic
and print the captured traffic only if the current test fails.

I think this thin adapter and the support code is getting too
complex. I am planning to try a different approach to get rid of
this thin adapter again for simplicity.

Change-Id: I3215ceea77b75c4f859ab5f948935f6485f28a11
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/306590
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
diff --git a/test/integration/goDebug.test.ts b/test/integration/goDebug.test.ts
index 9cfece7..11a30af 100644
--- a/test/integration/goDebug.test.ts
+++ b/test/integration/goDebug.test.ts
@@ -8,10 +8,11 @@
 import * as fs from 'fs';
 import * as http from 'http';
 import { tmpdir } from 'os';
+import * as net from 'net';
 import * as path from 'path';
 import * as sinon from 'sinon';
 import * as proxy from '../../src/goDebugFactory';
-import { DebugConfiguration } from 'vscode';
+import { DebugConfiguration, DebugProtocolMessage } from 'vscode';
 import { DebugClient } from 'vscode-debugadapter-testsupport';
 import { ILocation } from 'vscode-debugadapter-testsupport/lib/debugClient';
 import { DebugProtocol } from 'vscode-debugprotocol';
@@ -289,12 +290,11 @@
 
 // Test suite adapted from:
 // https://github.com/microsoft/vscode-mock-debug/blob/master/src/tests/adapter.test.ts
-const testAll = (isDlvDap: boolean) => {
+const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
 	// To disable skipping of dlvDapTests, set dlvDapSkipsEnabled = false.
 	const dlvDapSkipsEnabled = true;
 	const debugConfigProvider = new GoDebugConfigurationProvider();
 	const DEBUG_ADAPTER = path.join('.', 'out', 'src', 'debugAdapter', 'goDebug.js');
-	let dlvDapProcess: cp.ChildProcess;
 
 	const PROJECT_ROOT = path.normalize(path.join(__dirname, '..', '..', '..'));
 	const DATA_ROOT = path.join(PROJECT_ROOT, 'test', 'testdata');
@@ -309,18 +309,17 @@
 	};
 
 	let dc: DebugClient;
+	let dlvDapAdapter: DelveDAPDebugAdapterOnSocket;
 
 	setup(async () => {
 		if (isDlvDap) {
 			dc = new DebugClient('dlv', 'dap', 'go');
+			// dc.start will be called in initializeDebugConfig call,
+			// which creates a thin adapter for delve dap mode,
+			// runs it on a network port, and gets wired with this dc.
 
 			// Launching delve may take longer than the default timeout of 5000.
 			dc.defaultTimeout = 20_000;
-
-			// Change the output to be printed to the console.
-			sinon.stub(proxy, 'appendToDebugConsole').callsFake((msg: string) => {
-				console.log(msg);
-			});
 			return;
 		}
 
@@ -333,9 +332,14 @@
 
 	teardown(async () => {
 		await dc.stop();
-		if (dlvDapProcess) {
-			await killProcessTree(dlvDapProcess);
-			dlvDapProcess = null;
+		if (dlvDapAdapter) {
+			const d = dlvDapAdapter;
+			dlvDapAdapter = null;
+			if (ctx.currentTest?.state === 'failed') {
+				console.log(`${ctx.currentTest?.title} FAILED: DAP Trace`);
+				d.printLog();
+			}
+			await d.dispose();
 		}
 		sinon.restore();
 	});
@@ -1812,10 +1816,9 @@
 
 		const debugConfig = await debugConfigProvider.resolveDebugConfiguration(undefined, config);
 		if (isDlvDap) {
-			const { port, dlvDapServer } = await proxy.startDapServer(debugConfig);
-			dlvDapProcess = dlvDapServer;
-			debugConfig.port = port; // let the debug test client connect to our dap server.
-			await dc.start(port);
+			dlvDapAdapter = new DelveDAPDebugAdapterOnSocket(debugConfig);
+			const port = await dlvDapAdapter.serve();
+			await dc.start(port); // This will connect to the adapter's port.
 		}
 		return debugConfig;
 	}
@@ -1823,10 +1826,134 @@
 
 suite('Go Debug Adapter Tests (legacy)', function () {
 	this.timeout(60_000);
-	testAll(false);
+	testAll(this.ctx, false);
 });
 
 suite('Go Debug Adapter Tests (dlv-dap)', function () {
 	this.timeout(60_000);
-	testAll(true);
+	testAll(this.ctx, true);
 });
+
+// DelveDAPDebugAdapterOnSocket runs a DelveDAPOutputAdapter
+// over a network socket. This allows tests to instantiate
+// the thin adapter for Delve DAP and the debug test support's
+// DebugClient to communicate with the adapter over a network socket.
+class DelveDAPDebugAdapterOnSocket extends proxy.DelveDAPOutputAdapter {
+	constructor(config: DebugConfiguration) {
+		super(config, false);
+	}
+
+	private static TWO_CRLF = '\r\n\r\n';
+	private _rawData: Buffer;
+	private _contentLength: number;
+	private _writableStream: NodeJS.WritableStream;
+	private _server: net.Server;
+	private _port: number; // port for the thin adapter.
+
+	public serve(): Promise<number> {
+		return new Promise(async (resolve, reject) => {
+			this._port = await getPort();
+			this._server = net.createServer((c) => {
+				this.log('>> accepted connection from client');
+				c.on('end', () => {
+					this.log('>> client disconnected');
+					this.dispose();
+				});
+				this.run(c, c);
+			});
+			this._server.on('error', (err) => reject(err));
+			this._server.listen(this._port, () => resolve(this._port));
+		});
+	}
+
+	private run(inStream: NodeJS.ReadableStream, outStream: NodeJS.WritableStream): void {
+		this._writableStream = outStream;
+		this._rawData = Buffer.alloc(0);
+
+		// forward to DelveDAPDebugAdapter, which will forward to dlv dap.
+		inStream.on('data', (data: Buffer) => this._handleData(data));
+		// handle data from DelveDAPDebugAdapter, that's from dlv dap.
+		this.onDidSendMessage((m) => this._send(m));
+
+		inStream.resume();
+	}
+
+	private _disposed = false;
+	public async dispose() {
+		if (this._disposed) {
+			return;
+		}
+		this._disposed = true;
+		this.log('adapter disposed');
+		await this._server.close();
+		await super.dispose();
+	}
+
+	// Code from
+	// https://github.com/microsoft/vscode-debugadapter-node/blob/2235a2227d1a439372be578cd3f55e15211851b7/testSupport/src/protocolClient.ts#L96-L97
+	private _send(message: DebugProtocolMessage): void {
+		if (this._writableStream) {
+			const json = JSON.stringify(message);
+			this.log(`<- server: ${json}`);
+			if (!this._writableStream.writable) {
+				this.log('socket closed already');
+				return;
+			}
+			this._writableStream.write(
+				`Content-Length: ${Buffer.byteLength(json, 'utf8')}${DelveDAPDebugAdapterOnSocket.TWO_CRLF}${json}`,
+				'utf8'
+			);
+		}
+	}
+
+	// Code from
+	// https://github.com/microsoft/vscode-debugadapter-node/blob/2235a2227d1a439372be578cd3f55e15211851b7/testSupport/src/protocolClient.ts#L100-L132
+	private _handleData(data: Buffer): void {
+		this._rawData = Buffer.concat([this._rawData, data]);
+
+		// eslint-disable-next-line no-constant-condition
+		while (true) {
+			if (this._contentLength >= 0) {
+				if (this._rawData.length >= this._contentLength) {
+					const message = this._rawData.toString('utf8', 0, this._contentLength);
+					this._rawData = this._rawData.slice(this._contentLength);
+					this._contentLength = -1;
+					if (message.length > 0) {
+						try {
+							this.log(`-> server: ${message}`);
+							const msg: DebugProtocol.ProtocolMessage = JSON.parse(message);
+							this.handleMessage(msg);
+						} catch (e) {
+							throw new Error('Error handling data: ' + (e && e.message));
+						}
+					}
+					continue; // there may be more complete messages to process
+				}
+			} else {
+				const idx = this._rawData.indexOf(DelveDAPDebugAdapterOnSocket.TWO_CRLF);
+				if (idx !== -1) {
+					const header = this._rawData.toString('utf8', 0, idx);
+					const lines = header.split('\r\n');
+					for (let i = 0; i < lines.length; i++) {
+						const pair = lines[i].split(/: +/);
+						if (pair[0] === 'Content-Length') {
+							this._contentLength = +pair[1];
+						}
+					}
+					this._rawData = this._rawData.slice(idx + DelveDAPDebugAdapterOnSocket.TWO_CRLF.length);
+					continue;
+				}
+			}
+			break;
+		}
+	}
+
+	/* --- accumulate log messages so we can output when the test fails --- */
+	private _log = [] as string[];
+	private log(msg: string) {
+		this._log.push(msg);
+	}
+	public printLog() {
+		this._log.forEach((msg) => console.log(msg));
+	}
+}