Skip to content

Commit 471ed7e

Browse files
committed
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>
1 parent f8f5433 commit 471ed7e

File tree

1 file changed

+144
-17
lines changed

1 file changed

+144
-17
lines changed

test/integration/goDebug.test.ts

Lines changed: 144 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import * as cp from 'child_process';
88
import * as fs from 'fs';
99
import * as http from 'http';
1010
import { tmpdir } from 'os';
11+
import * as net from 'net';
1112
import * as path from 'path';
1213
import * as sinon from 'sinon';
1314
import * as proxy from '../../src/goDebugFactory';
14-
import { DebugConfiguration } from 'vscode';
15+
import { DebugConfiguration, DebugProtocolMessage } from 'vscode';
1516
import { DebugClient } from 'vscode-debugadapter-testsupport';
1617
import { ILocation } from 'vscode-debugadapter-testsupport/lib/debugClient';
1718
import { DebugProtocol } from 'vscode-debugprotocol';
@@ -289,12 +290,11 @@ suite('RemoteSourcesAndPackages Tests', () => {
289290

290291
// Test suite adapted from:
291292
// https://github.com/microsoft/vscode-mock-debug/blob/master/src/tests/adapter.test.ts
292-
const testAll = (isDlvDap: boolean) => {
293+
const testAll = (ctx: Mocha.Context, isDlvDap: boolean) => {
293294
// To disable skipping of dlvDapTests, set dlvDapSkipsEnabled = false.
294295
const dlvDapSkipsEnabled = true;
295296
const debugConfigProvider = new GoDebugConfigurationProvider();
296297
const DEBUG_ADAPTER = path.join('.', 'out', 'src', 'debugAdapter', 'goDebug.js');
297-
let dlvDapProcess: cp.ChildProcess;
298298

299299
const PROJECT_ROOT = path.normalize(path.join(__dirname, '..', '..', '..'));
300300
const DATA_ROOT = path.join(PROJECT_ROOT, 'test', 'testdata');
@@ -309,18 +309,17 @@ const testAll = (isDlvDap: boolean) => {
309309
};
310310

311311
let dc: DebugClient;
312+
let dlvDapAdapter: DelveDAPDebugAdapterOnSocket;
312313

313314
setup(async () => {
314315
if (isDlvDap) {
315316
dc = new DebugClient('dlv', 'dap', 'go');
317+
// dc.start will be called in initializeDebugConfig call,
318+
// which creates a thin adapter for delve dap mode,
319+
// runs it on a network port, and gets wired with this dc.
316320

317321
// Launching delve may take longer than the default timeout of 5000.
318322
dc.defaultTimeout = 20_000;
319-
320-
// Change the output to be printed to the console.
321-
sinon.stub(proxy, 'appendToDebugConsole').callsFake((msg: string) => {
322-
console.log(msg);
323-
});
324323
return;
325324
}
326325

@@ -333,9 +332,14 @@ const testAll = (isDlvDap: boolean) => {
333332

334333
teardown(async () => {
335334
await dc.stop();
336-
if (dlvDapProcess) {
337-
await killProcessTree(dlvDapProcess);
338-
dlvDapProcess = null;
335+
if (dlvDapAdapter) {
336+
const d = dlvDapAdapter;
337+
dlvDapAdapter = null;
338+
if (ctx.currentTest?.state === 'failed') {
339+
console.log(`${ctx.currentTest?.title} FAILED: DAP Trace`);
340+
d.printLog();
341+
}
342+
await d.dispose();
339343
}
340344
sinon.restore();
341345
});
@@ -1812,21 +1816,144 @@ const testAll = (isDlvDap: boolean) => {
18121816

18131817
const debugConfig = await debugConfigProvider.resolveDebugConfiguration(undefined, config);
18141818
if (isDlvDap) {
1815-
const { port, dlvDapServer } = await proxy.startDapServer(debugConfig);
1816-
dlvDapProcess = dlvDapServer;
1817-
debugConfig.port = port; // let the debug test client connect to our dap server.
1818-
await dc.start(port);
1819+
dlvDapAdapter = new DelveDAPDebugAdapterOnSocket(debugConfig);
1820+
const port = await dlvDapAdapter.serve();
1821+
await dc.start(port); // This will connect to the adapter's port.
18191822
}
18201823
return debugConfig;
18211824
}
18221825
};
18231826

18241827
suite('Go Debug Adapter Tests (legacy)', function () {
18251828
this.timeout(60_000);
1826-
testAll(false);
1829+
testAll(this.ctx, false);
18271830
});
18281831

18291832
suite('Go Debug Adapter Tests (dlv-dap)', function () {
18301833
this.timeout(60_000);
1831-
testAll(true);
1834+
testAll(this.ctx, true);
18321835
});
1836+
1837+
// DelveDAPDebugAdapterOnSocket runs a DelveDAPOutputAdapter
1838+
// over a network socket. This allows tests to instantiate
1839+
// the thin adapter for Delve DAP and the debug test support's
1840+
// DebugClient to communicate with the adapter over a network socket.
1841+
class DelveDAPDebugAdapterOnSocket extends proxy.DelveDAPOutputAdapter {
1842+
constructor(config: DebugConfiguration) {
1843+
super(config, false);
1844+
}
1845+
1846+
private static TWO_CRLF = '\r\n\r\n';
1847+
private _rawData: Buffer;
1848+
private _contentLength: number;
1849+
private _writableStream: NodeJS.WritableStream;
1850+
private _server: net.Server;
1851+
private _port: number; // port for the thin adapter.
1852+
1853+
public serve(): Promise<number> {
1854+
return new Promise(async (resolve, reject) => {
1855+
this._port = await getPort();
1856+
this._server = net.createServer((c) => {
1857+
this.log('>> accepted connection from client');
1858+
c.on('end', () => {
1859+
this.log('>> client disconnected');
1860+
this.dispose();
1861+
});
1862+
this.run(c, c);
1863+
});
1864+
this._server.on('error', (err) => reject(err));
1865+
this._server.listen(this._port, () => resolve(this._port));
1866+
});
1867+
}
1868+
1869+
private run(inStream: NodeJS.ReadableStream, outStream: NodeJS.WritableStream): void {
1870+
this._writableStream = outStream;
1871+
this._rawData = Buffer.alloc(0);
1872+
1873+
// forward to DelveDAPDebugAdapter, which will forward to dlv dap.
1874+
inStream.on('data', (data: Buffer) => this._handleData(data));
1875+
// handle data from DelveDAPDebugAdapter, that's from dlv dap.
1876+
this.onDidSendMessage((m) => this._send(m));
1877+
1878+
inStream.resume();
1879+
}
1880+
1881+
private _disposed = false;
1882+
public async dispose() {
1883+
if (this._disposed) {
1884+
return;
1885+
}
1886+
this._disposed = true;
1887+
this.log('adapter disposed');
1888+
await this._server.close();
1889+
await super.dispose();
1890+
}
1891+
1892+
// Code from
1893+
// https://github.com/microsoft/vscode-debugadapter-node/blob/2235a2227d1a439372be578cd3f55e15211851b7/testSupport/src/protocolClient.ts#L96-L97
1894+
private _send(message: DebugProtocolMessage): void {
1895+
if (this._writableStream) {
1896+
const json = JSON.stringify(message);
1897+
this.log(`<- server: ${json}`);
1898+
if (!this._writableStream.writable) {
1899+
this.log('socket closed already');
1900+
return;
1901+
}
1902+
this._writableStream.write(
1903+
`Content-Length: ${Buffer.byteLength(json, 'utf8')}${DelveDAPDebugAdapterOnSocket.TWO_CRLF}${json}`,
1904+
'utf8'
1905+
);
1906+
}
1907+
}
1908+
1909+
// Code from
1910+
// https://github.com/microsoft/vscode-debugadapter-node/blob/2235a2227d1a439372be578cd3f55e15211851b7/testSupport/src/protocolClient.ts#L100-L132
1911+
private _handleData(data: Buffer): void {
1912+
this._rawData = Buffer.concat([this._rawData, data]);
1913+
1914+
// eslint-disable-next-line no-constant-condition
1915+
while (true) {
1916+
if (this._contentLength >= 0) {
1917+
if (this._rawData.length >= this._contentLength) {
1918+
const message = this._rawData.toString('utf8', 0, this._contentLength);
1919+
this._rawData = this._rawData.slice(this._contentLength);
1920+
this._contentLength = -1;
1921+
if (message.length > 0) {
1922+
try {
1923+
this.log(`-> server: ${message}`);
1924+
const msg: DebugProtocol.ProtocolMessage = JSON.parse(message);
1925+
this.handleMessage(msg);
1926+
} catch (e) {
1927+
throw new Error('Error handling data: ' + (e && e.message));
1928+
}
1929+
}
1930+
continue; // there may be more complete messages to process
1931+
}
1932+
} else {
1933+
const idx = this._rawData.indexOf(DelveDAPDebugAdapterOnSocket.TWO_CRLF);
1934+
if (idx !== -1) {
1935+
const header = this._rawData.toString('utf8', 0, idx);
1936+
const lines = header.split('\r\n');
1937+
for (let i = 0; i < lines.length; i++) {
1938+
const pair = lines[i].split(/: +/);
1939+
if (pair[0] === 'Content-Length') {
1940+
this._contentLength = +pair[1];
1941+
}
1942+
}
1943+
this._rawData = this._rawData.slice(idx + DelveDAPDebugAdapterOnSocket.TWO_CRLF.length);
1944+
continue;
1945+
}
1946+
}
1947+
break;
1948+
}
1949+
}
1950+
1951+
/* --- accumulate log messages so we can output when the test fails --- */
1952+
private _log = [] as string[];
1953+
private log(msg: string) {
1954+
this._log.push(msg);
1955+
}
1956+
public printLog() {
1957+
this._log.forEach((msg) => console.log(msg));
1958+
}
1959+
}

0 commit comments

Comments
 (0)