blob: 04bbd9f4bac052bb5a90d7c1cff8aa85dc98a20c [file] [log] [blame]
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable node/no-unsupported-features/node-builtins */
/* eslint-disable no-async-promise-executor */
/* eslint-disable node/no-unpublished-import */
import * as assert from 'assert';
import * as cp from 'child_process';
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, DebugProtocolMessage } from 'vscode';
import { DebugClient } from 'vscode-debugadapter-testsupport';
import { ILocation } from 'vscode-debugadapter-testsupport/lib/debugClient';
import { DebugProtocol } from 'vscode-debugprotocol';
import {
Delve,
escapeGoModPath,
GoDebugSession,
PackageBuildInfo,
RemoteSourcesAndPackages
} from '../../src/debugAdapter/goDebug';
import { GoDebugConfigurationProvider } from '../../src/goDebugConfiguration';
import { getBinPath, rmdirRecursive } from '../../src/util';
import { killProcessTree } from '../../src/utils/processUtils';
import getPort = require('get-port');
import util = require('util');
import { parseProgramArgSync } from '../../src/goDebugFactory';
import { TimestampedLogger } from '../../src/goLogging';
// For debugging test and streaming the trace instead of buffering, set this.
const PRINT_TO_CONSOLE = false;
suite('Path Manipulation Tests', () => {
test('escapeGoModPath works', () => {
assert.strictEqual(escapeGoModPath('BurnSushi/test.go'), '!burn!sushi/test.go');
});
});
suite('GoDebugSession Tests', async () => {
const workspaceFolder = '/usr/workspacefolder';
const delve: Delve = {} as Delve;
let goDebugSession: GoDebugSession;
let remoteSourcesAndPackages: RemoteSourcesAndPackages;
let fileSystem: typeof fs;
let previousEnv: any;
setup(() => {
previousEnv = Object.assign({}, process.env);
process.env.GOPATH = '/usr/gopath';
process.env.GOROOT = '/usr/goroot';
remoteSourcesAndPackages = new RemoteSourcesAndPackages();
fileSystem = ({ existsSync: () => false } as unknown) as typeof fs;
delve.program = workspaceFolder;
delve.isApiV1 = false;
goDebugSession = new GoDebugSession(true, false, fileSystem);
goDebugSession['delve'] = delve;
goDebugSession['remoteSourcesAndPackages'] = remoteSourcesAndPackages;
});
teardown(() => {
process.env = previousEnv;
sinon.restore();
});
test('inferRemotePathFromLocalPath works', () => {
const sourceFileMapping = new Map<string, string[]>();
sourceFileMapping.set('main.go', ['/app/hello-world/main.go', '/app/main.go']);
sourceFileMapping.set('blah.go', ['/app/blah.go']);
remoteSourcesAndPackages.remoteSourceFilesNameGrouping = sourceFileMapping;
const inferredPath = goDebugSession['inferRemotePathFromLocalPath'](
'C:\\Users\\Documents\\src\\hello-world\\main.go'
);
assert.strictEqual(inferredPath, '/app/hello-world/main.go');
});
test('inferLocalPathFromRemoteGoPackage works for package in workspaceFolder', () => {
const remotePath = '/src/hello-world/morestrings/morestrings.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world/morestrings',
DirectoryPath: '/src/hello-world/morestrings',
Files: ['/src/hello-world/morestrings/lessstrings.go', '/src/hello-world/morestrings/morestrings.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'FooBar/test',
DirectoryPath: 'remote/pkg/mod/!foo!bar/test@v1.0.2',
Files: ['remote/pkg/mod/!foo!bar/test@v1.0.2/test.go']
};
const localPath = path.join(workspaceFolder, 'hello-world/morestrings/morestrings.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/pkg/mod', () => {
const remotePath = 'remote/pkg/mod/!foo!bar/test@v1.0.2/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'FooBar/test',
DirectoryPath: 'remote/pkg/mod/!foo!bar/test@v1.0.2',
Files: ['remote/pkg/mod/!foo!bar/test@v1.0.2/test.go']
};
const localPath = path.join(process.env.GOPATH, 'pkg/mod/!foo!bar/test@v1.0.2/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/pkg/mod with relative path', () => {
const remotePath = '!foo!bar/test@v1.0.2/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'FooBar/test',
DirectoryPath: '!foo!bar/test@v1.0.2',
Files: ['!foo!bar/test@v1.0.2/test.go']
};
const localPath = path.join(process.env.GOPATH, 'pkg/mod/!foo!bar/test@v1.0.2/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/src', () => {
const remotePath = 'remote/gopath/src/foobar/test@v1.0.2-abcde-34/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'foobar/test',
DirectoryPath: 'remote/gopath/src/foobar/test@v1.0.2-abcde-34',
Files: ['remote/gopath/src/foobar/test@v1.0.2-abcde-34/test.go']
};
const localPath = path.join(process.env.GOPATH, 'src', 'foobar/test@v1.0.2-abcde-34/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOPATH/src with relative path', () => {
const remotePath = 'foobar/test@v1.0.2/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'foobar/test',
DirectoryPath: 'foobar/test@v1.0.2',
Files: ['foobar/test@v1.0.2/test.go']
};
const localPath = path.join(process.env.GOPATH, 'src', 'foobar/test@v1.0.2/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOROOT/src', () => {
const remotePath = 'remote/goroot/src/foobar/test@v1.0.2/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'foobar/test',
DirectoryPath: 'remote/goroot/src/foobar/test@v1.0.2',
Files: ['remote/goroot/src/foobar/test@v1.0.2/test.go']
};
const localPath = path.join(process.env.GOROOT, 'src', 'foobar/test@v1.0.2/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
test('inferLocalPathFromRemoteGoPackage works for package in GOROOT/src with relative path', () => {
const remotePath = 'foobar/test@v1.0.2/test.go';
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'foobar/test',
DirectoryPath: 'foobar/test@v1.0.2',
Files: ['foobar/test@v1.0.2/test.go']
};
const localPath = path.join(process.env.GOROOT, 'src', 'foobar/test@v1.0.2/test.go');
const existsSyncStub = sinon.stub(fileSystem, 'existsSync');
existsSyncStub.withArgs(localPath).returns(true);
remoteSourcesAndPackages.remotePackagesBuildInfo = [helloPackage, testPackage];
goDebugSession['localPathSeparator'] = '/';
const inferredLocalPath = goDebugSession['inferLocalPathFromRemoteGoPackage'](remotePath);
assert.strictEqual(inferredLocalPath, localPath);
});
});
suite('RemoteSourcesAndPackages Tests', () => {
const helloPackage: PackageBuildInfo = {
ImportPath: 'hello-world',
DirectoryPath: '/src/hello-world',
Files: ['src/hello-world/hello.go', 'src/hello-world/world.go']
};
const testPackage: PackageBuildInfo = {
ImportPath: 'test',
DirectoryPath: '/src/test',
Files: ['src/test/test.go']
};
const sources = ['src/hello-world/hello.go', 'src/hello-world/world.go', 'src/test/test.go'];
let remoteSourcesAndPackages: RemoteSourcesAndPackages;
let delve: Delve;
setup(() => {
delve = ({ callPromise: () => ({}), isApiV1: false } as unknown) as Delve;
remoteSourcesAndPackages = new RemoteSourcesAndPackages();
});
teardown(() => {
sinon.restore();
});
test('initializeRemotePackagesAndSources retrieves remote packages and sources', async () => {
const stub = sinon.stub(delve, 'callPromise');
stub.withArgs('ListPackagesBuildInfo', [{ IncludeFiles: true }]).returns(
Promise.resolve({ List: [helloPackage, testPackage] })
);
stub.withArgs('ListSources', [{}]).returns(Promise.resolve({ Sources: sources }));
await remoteSourcesAndPackages.initializeRemotePackagesAndSources(delve);
assert.deepEqual(remoteSourcesAndPackages.remoteSourceFiles, sources);
assert.deepEqual(remoteSourcesAndPackages.remotePackagesBuildInfo, [helloPackage, testPackage]);
});
});
// Test suite adapted from:
// https://github.com/microsoft/vscode-mock-debug/blob/master/src/tests/adapter.test.ts
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');
const PROJECT_ROOT = path.normalize(path.join(__dirname, '..', '..', '..'));
const DATA_ROOT = path.join(PROJECT_ROOT, 'test', 'testdata');
const remoteAttachConfig = {
name: 'Attach',
type: 'go',
request: 'attach',
mode: 'remote',
host: '127.0.0.1',
port: 3456
};
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;
return;
}
dc = new DebugClient('node', path.join(PROJECT_ROOT, DEBUG_ADAPTER), 'go', undefined, true);
// Launching delve may take longer than the default timeout of 5000.
dc.defaultTimeout = 20_000;
// To connect to a running debug server for debugging the tests, specify PORT.
await dc.start();
});
teardown(() => {
if (dlvDapAdapter) {
const d = dlvDapAdapter;
dlvDapAdapter = null;
if (ctx.currentTest?.state === 'failed') {
console.log(`${ctx.currentTest?.title} FAILED: DAP Trace`);
d.printLog();
}
d.dispose();
} else {
dc?.stop();
}
sinon.restore();
});
/**
* This function sets up a server that returns helloworld on serverPort.
* The server will be started as a Delve remote headless instance
* that will listen on the specified dlvPort.
* We are using a server as opposed to a long-running program
* because we can use responses to better test when the program is
* running vs stopped/killed.
*/
async function setUpRemoteProgram(
dlvPort: number,
serverPort: number,
acceptMultiClient = true,
continueOnStart = true
): Promise<cp.ChildProcess> {
const serverFolder = path.join(DATA_ROOT, 'helloWorldServer');
const toolPath = getBinPath('dlv');
const args = ['debug', '--api-version=2', '--headless', `--listen=127.0.0.1:${dlvPort}`];
if (acceptMultiClient) {
args.push('--accept-multiclient');
}
if (continueOnStart) {
args.push('--continue');
}
const childProcess = cp.spawn(toolPath, args, {
cwd: serverFolder,
env: { PORT: `${serverPort}`, ...process.env }
});
// Give dlv a few seconds to start.
await new Promise((resolve) => setTimeout(resolve, 10_000));
return childProcess;
}
/**
* Helper function to set up remote attach configuration.
* This will issue an initializeRequest, followed by attachRequest.
* It will then wait for an initializedEvent before sending a breakpointRequest
* if breakpoints are provided. Lastly the configurationDoneRequest will be sent.
* NOTE: For simplicity, this function assumes the breakpoints are in the same file.
*/
async function setUpRemoteAttach(config: DebugConfiguration, breakpoints: ILocation[] = []): Promise<void> {
const debugConfig = await initializeDebugConfig(config);
console.log('Sending initializing request for remote attach setup.');
const initializedResult = await dc.initializeRequest();
assert.ok(initializedResult.success);
// When the attach request is completed successfully, we should get
// an initialized event.
await Promise.all([
new Promise<void>(async (resolve) => {
console.log(`Setting up attach request for ${JSON.stringify(debugConfig)}.`);
const attachResult = await dc.attachRequest(debugConfig as DebugProtocol.AttachRequestArguments);
assert.ok(attachResult.success);
resolve();
}),
dc.waitForEvent('initialized')
]);
if (breakpoints.length) {
console.log('Sending set breakpoints request for remote attach setup.');
const breakpointsResult = await dc.setBreakpointsRequest({
source: { path: breakpoints[0].path },
breakpoints
});
assert.ok(breakpointsResult.success && breakpointsResult.body.breakpoints.length === breakpoints.length);
// Verify that there are no non-verified breakpoints.
breakpointsResult.body.breakpoints.forEach((breakpoint) => {
assert.ok(breakpoint.verified);
});
}
console.log('Sending configuration done request for remote attach setup.');
const configurationDoneResult = await dc.configurationDoneRequest();
assert.ok(configurationDoneResult.success);
}
/**
* Helper function to retrieve a stopped event for a breakpoint.
* This function will keep calling action() until we receive a stoppedEvent.
* Will return undefined if the result of repeatedly calling action does not
* induce a stoppedEvent.
*/
async function waitForBreakpoint(action: () => void, breakpoint: ILocation): Promise<void> {
const assertStoppedLocation = dc.assertStoppedLocation('breakpoint', breakpoint);
await new Promise((res) => setTimeout(res, 1_000));
action();
await assertStoppedLocation;
}
/**
* Helper function to assert that a variable has a particular value.
* This should be called when the program is stopped.
*
* The following requests are issued by this function to determine the
* value of the variable:
* 1. threadsRequest
* 2. stackTraceRequest
* 3. scopesRequest
* 4. variablesRequest
*/
async function assertLocalVariableValue(name: string, val: string): Promise<void> {
const threadsResponse = await dc.threadsRequest();
assert(threadsResponse.success);
const stackTraceResponse = await dc.stackTraceRequest({ threadId: threadsResponse.body.threads[0].id });
assert(stackTraceResponse.success);
const scopesResponse = await dc.scopesRequest({ frameId: stackTraceResponse.body.stackFrames[0].id });
assert(scopesResponse.success);
const localScopeIndex = scopesResponse.body.scopes.findIndex((v) => v.name === 'Local' || v.name === 'Locals');
assert(localScopeIndex >= 0, "no scope named 'Local':");
const variablesResponse = await dc.variablesRequest({
variablesReference: scopesResponse.body.scopes[localScopeIndex].variablesReference
});
assert(variablesResponse.success);
// Locate the variable with the matching name.
const i = variablesResponse.body.variables.findIndex((v) => v.name === name);
assert(i >= 0, `no variable in scope named ${name}`);
// Check that the value of name is val.
assert.strictEqual(variablesResponse.body.variables[i].value, val);
}
suite('basic', () => {
test('unknown request should produce error', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
if (isDlvDap) {
const config = { name: 'Launch', type: 'go', request: 'launch', program: DATA_ROOT };
await initializeDebugConfig(config);
}
try {
await dc.send('illegal_request');
} catch {
return;
}
throw new Error('does not report error on unknown request');
});
});
suite('initialize', () => {
test('should return supported features', async () => {
if (isDlvDap) {
const config = { name: 'Launch', type: 'go', request: 'launch', program: DATA_ROOT };
await initializeDebugConfig(config);
}
await dc.initializeRequest().then((response) => {
response.body = response.body || {};
assert.strictEqual(response.body.supportsConditionalBreakpoints, true);
assert.strictEqual(response.body.supportsConfigurationDoneRequest, true);
if (!isDlvDap) {
// not supported in dlv-dap
assert.strictEqual(response.body.supportsSetVariable, true);
}
});
});
test("should produce error for invalid 'pathFormat'", async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
if (isDlvDap) {
const config = { name: 'Launch', type: 'go', request: 'launch', program: DATA_ROOT };
await initializeDebugConfig(config);
}
try {
await dc.initializeRequest({
adapterID: 'mock',
linesStartAt1: true,
columnsStartAt1: true,
pathFormat: 'url'
});
} catch (err) {
return; // want error
}
throw new Error("does not report error on invalid 'pathFormat' attribute");
});
});
suite('launch', () => {
test('should run program to the end', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
});
test('should stop on entry', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: true
};
const debugConfig = await initializeDebugConfig(config);
await 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');
})
]);
});
test('should debug a file', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest', 'test.go');
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
});
test('should debug a single test', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'test',
program: PROGRAM,
args: ['-test.run', 'TestMe']
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
});
test('should debug a test package', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'test',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
});
test('invalid flags are passed to dlv but should be caught by dlv', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
dlvFlags: ['--invalid']
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc.assertOutput('stderr', 'Error: unknown flag: --invalid\n', 5000),
dc.waitForEvent('terminated'),
dc.initializeRequest().then((response) => {
// The current debug adapter does not respond to launch request but,
// instead, sends error messages and TerminatedEvent as delve is closed.
// The promise from dc.launchRequest resolves when the launch response
// is received, so the promise will never get resolved.
dc.launchRequest(debugConfig as any);
})
]);
});
test('should handle threads request after initialization', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc.configurationSequence().then(() => {
dc.threadsRequest().then((response) => {
assert.ok(response.success);
});
}),
dc.launch(debugConfig),
dc.waitForEvent('terminated')
]);
});
test('should handle delayed initial threads request', async () => {
// If the program exits very quickly, the initial threadsRequest
// will complete after it has exited.
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
const response = await dc.threadsRequest();
assert.ok(response.success);
});
test('user-specified --listen flag should be ignored', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
dlvFlags: ['--listen=127.0.0.1:80']
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
});
});
suite('set current working directory', () => {
test('should debug program with cwd set', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest');
const FILE = path.join(PROGRAM, 'main.go');
const BREAKPOINT_LINE = 11;
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
cwd: WD
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
await assertLocalVariableValue('strdat', '"Hello, World!"');
});
test('should debug program without cwd set', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest');
const FILE = path.join(PROGRAM, 'main.go');
const BREAKPOINT_LINE = 11;
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));
await assertLocalVariableValue('strdat', '"Goodbye, World."');
});
test('should debug file program with cwd set', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
const FILE = PROGRAM;
const BREAKPOINT_LINE = 11;
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
cwd: WD
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
await assertLocalVariableValue('strdat', '"Hello, World!"');
});
test('should debug file program without cwd set', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
const FILE = PROGRAM;
const BREAKPOINT_LINE = 11;
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));
await assertLocalVariableValue('strdat', '"Goodbye, World."');
});
async function waitForHelloGoodbyeOutput(dc: DebugClient): Promise<DebugProtocol.Event> {
return await new Promise<DebugProtocol.Event>((resolve, reject) => {
dc.on('output', (event) => {
if (event.body.output === 'Hello, World!\n' || event.body.output === 'Goodbye, World.\n') {
// Resolve when we have found the event that we want.
resolve(event);
return;
}
});
});
}
test('should run program with cwd set (noDebug)', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
cwd: WD,
noDebug: true
};
const debugConfig = await initializeDebugConfig(config);
dc.launch(debugConfig);
const event = await waitForHelloGoodbyeOutput(dc);
assert.strictEqual(event.body.output, 'Hello, World!\n');
});
test('should run program without cwd set (noDebug)', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
noDebug: true
};
const debugConfig = await initializeDebugConfig(config);
dc.launch(debugConfig);
const event = await waitForHelloGoodbyeOutput(dc);
assert.strictEqual(event.body.output, 'Goodbye, World.\n');
});
test('should run file program with cwd set (noDebug)', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
cwd: WD,
noDebug: true
};
const debugConfig = await initializeDebugConfig(config);
dc.launch(debugConfig);
const event = await waitForHelloGoodbyeOutput(dc);
assert.strictEqual(event.body.output, 'Hello, World!\n');
});
test('should run file program without cwd set (noDebug)', async () => {
const WD = path.join(DATA_ROOT, 'cwdTest');
const PROGRAM = path.join(WD, 'cwdTest', 'main.go');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
noDebug: true
};
const debugConfig = await initializeDebugConfig(config);
dc.launch(debugConfig);
const event = await waitForHelloGoodbyeOutput(dc);
assert.strictEqual(event.body.output, 'Goodbye, World.\n');
});
});
suite('remote attach', () => {
let childProcess: cp.ChildProcess;
let server: number;
let debugConfig: DebugConfiguration;
setup(async () => {
server = await getPort();
remoteAttachConfig.port = await getPort();
debugConfig = remoteAttachConfig;
});
teardown(async () => {
await dc.stop();
await killProcessTree(childProcess);
// Wait 2 seconds for the process to be killed.
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
test('can connect and initialize using external dlv --headless --accept-multiclient=true --continue=true', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
childProcess = await setUpRemoteProgram(remoteAttachConfig.port, server, true, true);
await setUpRemoteAttach(debugConfig);
});
test('can connect and initialize using external dlv --headless --accept-multiclient=false --continue=false', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
childProcess = await setUpRemoteProgram(remoteAttachConfig.port, server, false, false);
await setUpRemoteAttach(debugConfig);
});
test('can connect and initialize using external dlv --headless --accept-multiclient=true --continue=false', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
childProcess = await setUpRemoteProgram(remoteAttachConfig.port, server, true, false);
await setUpRemoteAttach(debugConfig);
});
});
// The file paths returned from delve use '/' not the native path
// separator, so we can replace any instances of '\' with '/', which
// allows the hitBreakpoint check to match.
const getBreakpointLocation = (FILE: string, LINE: number) => {
return { path: FILE.replace(/\\/g, '/'), line: LINE };
};
suite('setBreakpoints', () => {
let server: number;
let remoteAttachDebugConfig: DebugConfiguration;
setup(async () => {
server = await getPort();
remoteAttachConfig.port = await getPort();
remoteAttachDebugConfig = remoteAttachConfig;
});
test('should stop on a breakpoint', async () => {
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: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
});
test('should stop on a breakpoint in test file', async () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
const FILE = path.join(DATA_ROOT, 'baseTest', 'sample_test.go');
const BREAKPOINT_LINE = 15;
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'test',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
});
test('stopped for a breakpoint set during initialization (remote attach)', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const FILE = path.join(DATA_ROOT, 'helloWorldServer', 'main.go');
const BREAKPOINT_LINE = 29;
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE);
// Setup attach with a breakpoint.
await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
// Calls the helloworld server to make the breakpoint hit.
await waitForBreakpoint(
() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
breakpointLocation
);
await dc.disconnectRequest({ restart: false });
await killProcessTree(remoteProgram);
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
test('stopped for a breakpoint set after initialization (remote attach)', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const FILE = path.join(DATA_ROOT, 'helloWorldServer', 'main.go');
const BREAKPOINT_LINE = 29;
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
// Setup attach without a breakpoint.
await setUpRemoteAttach(remoteAttachDebugConfig);
// Now sets a breakpoint.
const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE);
const breakpointsResult = await dc.setBreakpointsRequest({
source: { path: breakpointLocation.path },
breakpoints: [breakpointLocation]
});
assert.ok(breakpointsResult.success && breakpointsResult.body.breakpoints[0].verified);
// Calls the helloworld server to make the breakpoint hit.
await waitForBreakpoint(
() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
breakpointLocation
);
await dc.disconnectRequest({ restart: false });
await killProcessTree(remoteProgram);
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
test('should set breakpoints during continue', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'sleep');
const FILE = path.join(DATA_ROOT, 'sleep', 'sleep.go');
const HELLO_LINE = 10;
const helloLocation = getBreakpointLocation(FILE, HELLO_LINE);
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
await Promise.all([
dc.setBreakpointsRequest({
lines: [helloLocation.line],
breakpoints: [{ line: helloLocation.line, column: 0 }],
source: { path: helloLocation.path }
}),
dc.assertStoppedLocation('breakpoint', helloLocation)
]);
});
async function setBreakpointsDuringStep(nextFunc: () => void) {
const PROGRAM = path.join(DATA_ROOT, 'sleep');
const FILE = path.join(DATA_ROOT, 'sleep', 'sleep.go');
const SLEEP_LINE = 11;
const setupBreakpoint = getBreakpointLocation(FILE, SLEEP_LINE);
const HELLO_LINE = 10;
const onNextBreakpoint = getBreakpointLocation(FILE, HELLO_LINE);
const config = {
name: 'Launch file',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, setupBreakpoint);
// The program is now stopped at the line containing time.Sleep().
// Issue a next request, followed by a setBreakpointsRequest.
nextFunc();
// Note: the current behavior of setting a breakpoint during a next
// request will cause the step to be interrupted, so it may not be
// stopped on the next line.
await Promise.all([
dc.setBreakpointsRequest({
lines: [onNextBreakpoint.line],
breakpoints: [{ line: onNextBreakpoint.line, column: 0 }],
source: { path: onNextBreakpoint.path }
}),
dc.assertStoppedLocation('next cancelled', {})
]);
// Once the 'step' has completed, continue the program and
// make sure the breakpoint set while the program was nexting
// is succesfully hit.
await Promise.all([
dc.continueRequest({ threadId: 1 }),
dc.assertStoppedLocation('breakpoint', onNextBreakpoint)
]);
}
test('should set breakpoints during next', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // Skipped due to github.com/golang/vscode-go/issues/1390
}
await setBreakpointsDuringStep(async () => {
const nextResponse = await dc.nextRequest({ threadId: 1 });
assert.ok(nextResponse.success);
});
});
test('should set breakpoints during step out', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // Skipped due to github.com/golang/vscode-go/issues/1390
}
await setBreakpointsDuringStep(async () => {
const stepOutResponse = await dc.stepOutRequest({ threadId: 1 });
assert.ok(stepOutResponse.success);
});
});
});
suite('conditionalBreakpoints', () => {
test('should stop on conditional breakpoint', async () => {
const PROGRAM = path.join(DATA_ROOT, 'condbp');
const FILE = path.join(DATA_ROOT, 'condbp', 'condbp.go');
const BREAKPOINT_LINE = 7;
const location = getBreakpointLocation(FILE, BREAKPOINT_LINE);
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc
.waitForEvent('initialized')
.then(() => {
return dc.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, condition: 'i == 2' }],
source: { path: location.path }
});
})
.then(() => {
return dc.configurationDoneRequest();
}),
dc.launch(debugConfig),
dc.assertStoppedLocation('breakpoint', location)
]).then(() =>
// The program is stopped at the breakpoint, check to make sure 'i == 1'.
assertLocalVariableValue('i', '2')
);
});
test('should add breakpoint condition', async () => {
const PROGRAM = path.join(DATA_ROOT, 'condbp');
const FILE = path.join(DATA_ROOT, 'condbp', 'condbp.go');
const BREAKPOINT_LINE = 7;
const location = getBreakpointLocation(FILE, BREAKPOINT_LINE);
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await dc
.hitBreakpoint(debugConfig, location)
.then(() =>
// The program is stopped at the breakpoint, check to make sure 'i == 0'.
assertLocalVariableValue('i', '0')
)
.then(() =>
// Add a condition to the breakpoint, and make sure it runs until 'i == 2'.
dc
.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, condition: 'i == 2' }],
source: { path: location.path }
})
.then(() =>
Promise.all([
dc.continueRequest({ threadId: 1 }),
dc.assertStoppedLocation('breakpoint', location)
]).then(() =>
// The program is stopped at the breakpoint, check to make sure 'i == 2'.
assertLocalVariableValue('i', '2')
)
)
);
});
test('should remove breakpoint condition', async () => {
const PROGRAM = path.join(DATA_ROOT, 'condbp');
const FILE = path.join(DATA_ROOT, 'condbp', 'condbp.go');
const BREAKPOINT_LINE = 7;
const location = getBreakpointLocation(FILE, BREAKPOINT_LINE);
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc
.waitForEvent('initialized')
.then(async () => {
return dc.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line, condition: 'i == 2' }],
source: { path: location.path }
});
})
.then(() => {
return dc.configurationDoneRequest();
}),
dc.launch(debugConfig),
dc.assertStoppedLocation('breakpoint', location)
])
.then(() =>
// The program is stopped at the breakpoint, check to make sure 'i == 2'.
assertLocalVariableValue('i', '2')
)
.then(() =>
// Remove the breakpoint condition, and make sure the program runs until 'i == 3'.
dc
.setBreakpointsRequest({
lines: [location.line],
breakpoints: [{ line: location.line }],
source: { path: location.path }
})
.then(() =>
Promise.all([
dc.continueRequest({ threadId: 1 }),
dc.assertStoppedLocation('breakpoint', location)
]).then(() =>
// The program is stopped at the breakpoint, check to make sure 'i == 3'.
assertLocalVariableValue('i', '3')
)
)
);
});
});
suite('panicBreakpoints', () => {
test('should stop on panic', async () => {
const PROGRAM_WITH_EXCEPTION = path.join(DATA_ROOT, 'panic');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM_WITH_EXCEPTION
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc
.waitForEvent('initialized')
.then(() => {
return dc.setExceptionBreakpointsRequest({
filters: ['all']
});
})
.then(() => {
return dc.configurationDoneRequest();
}),
dc.launch(debugConfig),
dc.assertStoppedLocation('panic', {})
]);
});
test('should stop on runtime error during continue', async function () {
if (!isDlvDap) {
// Not implemented in the legacy adapter.
this.skip();
}
const PROGRAM_WITH_EXCEPTION = path.join(DATA_ROOT, 'runtimeError');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM_WITH_EXCEPTION
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([
dc.configurationSequence(),
dc.launch(debugConfig),
dc.waitForEvent('stopped').then((event) => {
assert(event.body.reason === 'runtime error' || event.body.reason === 'panic');
})
]);
});
test('should stop on runtime error during next', async function () {
if (!isDlvDap) {
// Not implemented in the legacy adapter.
this.skip();
}
const PROGRAM_WITH_EXCEPTION = path.join(DATA_ROOT, 'runtimeError');
const FILE = path.join(PROGRAM_WITH_EXCEPTION, 'oops.go');
const BREAKPOINT_LINE = 5;
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM_WITH_EXCEPTION
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
await Promise.all([
dc.nextRequest({ threadId: 1 }),
dc.waitForEvent('stopped').then((event) => {
assert(event.body.reason === 'runtime error' || event.body.reason === 'panic');
})
]);
});
});
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 function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const server = await getPort();
remoteAttachConfig.port = await getPort();
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
// Setup attach.
await setUpRemoteAttach(remoteAttachConfig);
// Calls the helloworld server to get a response.
let response = '';
await new Promise<void>((resolve) => {
http.get(`http://localhost:${server}`, (res) => {
res.on('data', (data) => (response += data));
res.on('end', () => resolve());
});
});
await dc.disconnectRequest();
// Checks that after the disconnect, the helloworld server still works.
let secondResponse = '';
await new Promise<void>((resolve) => {
http.get(`http://localhost:${server}`, (res) => {
res.on('data', (data) => (secondResponse += data));
res.on('end', () => resolve());
});
});
assert.strictEqual(response, 'Hello, world!');
assert.strictEqual(response, secondResponse);
await killProcessTree(remoteProgram);
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
test('should disconnect while continuing on entry', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: false
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect with multiple disconnectRequests', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: false
};
const debugConfig = await initializeDebugConfig(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 function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: true
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
const continueResponse = await dc.continueRequest({ threadId: 1 });
assert.ok(continueResponse.success);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect while nexting', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
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: 'debug',
program: PROGRAM,
stopOnEntry: false
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, location);
const nextResponse = await dc.nextRequest({ threadId: 1 });
assert.ok(nextResponse.success);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect while paused on pause', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
const pauseResponse = await dc.pauseRequest({ threadId: 1 });
assert.ok(pauseResponse.success);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect while paused on breakpoint', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
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: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect while paused on entry', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: true
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should disconnect while paused on next', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: true
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
const nextResponse = await dc.nextRequest({ threadId: 1 });
assert.ok(nextResponse.success);
await Promise.all([dc.disconnectRequest({ restart: false }), dc.waitForEvent('terminated')]);
});
test('should cleanup when stopped', async function () {
if (!isDlvDap || !dlvDapSkipsEnabled) {
this.skip();
}
const PROGRAM = path.join(DATA_ROOT, 'loop');
const OUTPUT = path.join(PROGRAM, '_loop_output');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
stopOnEntry: false,
output: OUTPUT
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig)]);
try {
const fsstat = util.promisify(fs.stat);
await fsstat(OUTPUT);
} catch (e) {
assert.fail(`debug output ${OUTPUT} wasn't built: ${e}`);
}
// Skip the proper disconnect sequence started with a disconnect request.
await dlvDapAdapter.dispose(1);
dc = undefined;
await sleep(100); // allow dlv to respond and finish cleanup.
let stat: fs.Stats = null;
try {
const fsstat = util.promisify(fs.stat);
stat = await fsstat(OUTPUT);
fs.unlinkSync(OUTPUT);
} catch (e) {
console.log(`output was cleaned ${OUTPUT} ${e}`);
}
assert.strictEqual(stat, null, `debug output ${OUTPUT} wasn't cleaned up. ${JSON.stringify(stat)}`);
console.log('finished');
});
});
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_MAIN_RUN1 = 6;
const BREAKPOINT_LINE_MAIN_RUN2 = 14;
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM
};
const debugConfig = await initializeDebugConfig(config);
// Set a breakpoint in run 1 and get the goroutine id.
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE_MAIN_RUN1));
const threadsResponse1 = await dc.threadsRequest();
assert.ok(threadsResponse1.success);
const run1Goroutine = threadsResponse1.body.threads.find((val) => val.name.indexOf('main.run1') >= 0);
// Set a breakpoint in run 2 and get the goroutine id.
// By setting breakpoints in both goroutine, we can make sure that both goroutines
// are running before continuing.
const bp2 = getBreakpointLocation(FILE, BREAKPOINT_LINE_MAIN_RUN2);
const breakpointsResult = await dc.setBreakpointsRequest({
source: { path: bp2.path },
breakpoints: [{ line: bp2.line }]
});
assert.ok(breakpointsResult.success);
const threadsResponse2 = await dc.threadsRequest();
assert.ok(threadsResponse2.success);
const run2Goroutine = threadsResponse2.body.threads.find((val) => val.name.indexOf('main.run2') >= 0);
await Promise.all([dc.continueRequest({ threadId: 1 }), dc.assertStoppedLocation('breakpoint', bp2)]);
// Clear breakpoints to make sure they do not interrupt the stepping.
const clearBreakpointsResult = await dc.setBreakpointsRequest({
source: { path: FILE },
breakpoints: []
});
assert.ok(clearBreakpointsResult.success);
// 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('next', async function () {
if (!isDlvDap) {
// Not implemented in the legacy adapter.
this.skip();
}
await runSwitchGoroutineTest('next');
});
test('step in', async function () {
if (!isDlvDap) {
// Not implemented in the legacy adapter.
this.skip();
}
// neither debug adapter implements this behavior
await runSwitchGoroutineTest('step in');
});
});
suite('logDest attribute tests', () => {
const PROGRAM = path.join(DATA_ROOT, 'baseTest');
let tmpDir: string;
suiteSetup(() => {
tmpDir = fs.mkdtempSync(path.join(tmpdir(), 'logDestTest'));
});
suiteTeardown(() => {
rmdirRecursive(tmpDir);
});
test('logs are written to logDest file', async function () {
if (!isDlvDap || process.platform === 'win32') {
this.skip();
}
const DELVE_LOG = path.join(tmpDir, 'delve.log');
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
logDest: DELVE_LOG
};
const debugConfig = await initializeDebugConfig(config);
await Promise.all([dc.configurationSequence(), dc.launch(debugConfig), dc.waitForEvent('terminated')]);
await dc.stop();
dc = undefined;
const dapLog = fs.readFileSync(DELVE_LOG)?.toString();
assert(
dapLog.includes('DAP server listening at') &&
dapLog.includes('"command":"initialize"') &&
dapLog.includes('"event":"terminated"'),
dapLog
);
});
async function testWithInvalidLogDest(logDest: any, wantedErrorMessage: string) {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'debug',
program: PROGRAM,
logDest
};
try {
await initializeDebugConfig(config);
} catch (error) {
assert(error?.message.includes(wantedErrorMessage), `unexpected error: ${error}`);
return;
}
assert.fail('dlv dap started normally, wanted the invalid logDest to cause failure');
}
test('relative path as logDest triggers an error', async function () {
if (!isDlvDap || process.platform === 'win32') this.skip();
await testWithInvalidLogDest('delve.log', 'relative path');
});
test('number as logDest triggers an error', async function () {
if (!isDlvDap || process.platform === 'win32') this.skip();
await testWithInvalidLogDest(3, 'file descriptor');
});
});
suite('substitute path', () => {
// TODO(suzmue): add unit tests for substitutePath.
let tmpDir: string;
suiteSetup(() => {
tmpDir = fs.mkdtempSync(path.join(DATA_ROOT, 'substitutePathTest'));
});
suiteTeardown(() => {
rmdirRecursive(tmpDir);
});
function copyDirectory(name: string) {
const from = path.join(DATA_ROOT, name);
const to = path.join(tmpDir, name);
fs.mkdirSync(to);
fs.readdirSync(from).forEach((file) => {
fs.copyFileSync(path.join(from, file), path.join(to, file));
});
return to;
}
async function buildGoProgram(cwd: string, outputFile: string): Promise<string> {
const goRuntimePath = getBinPath('go');
const execFile = util.promisify(cp.execFile);
const child = await execFile(goRuntimePath, ['build', '-o', outputFile, "--gcflags='all=-N -l'", '.'], {
cwd
});
if (child.stderr.length > 0) {
throw Error(child.stderr);
}
return outputFile;
}
suite('substitutePath with missing files', () => {
let goBuildOutput: string;
suiteSetup(() => {
goBuildOutput = fs.mkdtempSync(path.join(tmpdir(), 'output'));
});
suiteTeardown(() => {
rmdirRecursive(goBuildOutput);
});
async function copyBuildDelete(program: string): Promise<{ program: string; output: string }> {
const wd = copyDirectory(program);
const output = await buildGoProgram(wd, path.join(goBuildOutput, program));
rmdirRecursive(wd);
return { program: wd, output };
}
test('should stop on a breakpoint set in file with substituted path', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const { program, output } = await copyBuildDelete('baseTest');
const FILE = path.join(DATA_ROOT, 'baseTest', 'test.go');
const BREAKPOINT_LINE = 11;
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'exec',
program: output,
substitutePath: [
{
from: path.join(DATA_ROOT, 'baseTest'),
to: program
}
]
};
const debugConfig = await initializeDebugConfig(config);
await dc.hitBreakpoint(debugConfig, getBreakpointLocation(FILE, BREAKPOINT_LINE));
});
});
suite('substitutePath with remote program', () => {
let server: number;
let remoteAttachDebugConfig: DebugConfiguration;
let helloWorldLocal: string;
let helloWorldRemote: string;
setup(async () => {
server = await getPort();
remoteAttachConfig.port = await getPort();
remoteAttachDebugConfig = remoteAttachConfig;
});
suiteSetup(() => {
helloWorldLocal = copyDirectory('helloWorldServer');
helloWorldRemote = path.join(DATA_ROOT, 'helloWorldServer');
});
suiteTeardown(() => {
rmdirRecursive(helloWorldLocal);
});
test('stopped for a breakpoint set during initialization using substitutePath (remote attach)', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const FILE = path.join(helloWorldLocal, 'main.go');
const BREAKPOINT_LINE = 29;
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE);
// Setup attach with a breakpoint.
remoteAttachDebugConfig.cwd = tmpDir;
remoteAttachDebugConfig.remotePath = '';
remoteAttachDebugConfig.substitutePath = [{ from: helloWorldLocal, to: helloWorldRemote }];
await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
// Calls the helloworld server to make the breakpoint hit.
await waitForBreakpoint(
() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
breakpointLocation
);
await dc.disconnectRequest({ restart: false });
await killProcessTree(remoteProgram);
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
// Skip because it times out in nightly release workflow.
// BUG(https://github.com/golang/vscode-go/issues/1043)
test.skip('stopped for a breakpoint set during initialization using remotePath (remote attach)', async function () {
if (isDlvDap && dlvDapSkipsEnabled) {
this.skip(); // not working in dlv-dap.
}
const FILE = path.join(helloWorldLocal, 'main.go');
const BREAKPOINT_LINE = 29;
const remoteProgram = await setUpRemoteProgram(remoteAttachConfig.port, server);
const breakpointLocation = getBreakpointLocation(FILE, BREAKPOINT_LINE);
// Setup attach with a breakpoint.
remoteAttachDebugConfig.cwd = helloWorldLocal;
remoteAttachDebugConfig.remotePath = helloWorldRemote;
// This is a bad mapping, make sure that the remotePath config is used first.
remoteAttachDebugConfig.substitutePath = [{ from: helloWorldLocal, to: helloWorldLocal }];
await setUpRemoteAttach(remoteAttachDebugConfig, [breakpointLocation]);
// Calls the helloworld server to make the breakpoint hit.
await waitForBreakpoint(
() => http.get(`http://localhost:${server}`).on('error', (data) => console.log(data)),
breakpointLocation
);
await dc.disconnectRequest({ restart: false });
await killProcessTree(remoteProgram);
await new Promise((resolve) => setTimeout(resolve, 2_000));
});
});
});
let testNumber = 0;
async function initializeDebugConfig(config: DebugConfiguration) {
if (isDlvDap) {
config['debugAdapter'] = 'dlv-dap';
// Log the output for easier test debugging.
config['logOutput'] = 'dap';
config['showLog'] = true;
config['trace'] = 'verbose';
}
// Give each test a distinct debug binary. If a previous test
// and a new test use the same binary location, it is possible
// that the second test could build the binary, and then the
// first test could delete that binary during cleanup before the
// second test has a chance to run it.
if (!config['output'] && config['mode'] !== 'remote') {
const dir = parseProgramArgSync(config).dirname;
config['output'] = path.join(dir, `__debug_bin_${testNumber}`);
}
testNumber++;
const debugConfig = await debugConfigProvider.resolveDebugConfiguration(undefined, config);
if (isDlvDap) {
dlvDapAdapter = await DelveDAPDebugAdapterOnSocket.create(debugConfig);
const port = await dlvDapAdapter.serve();
await dc.start(port); // This will connect to the adapter's port.
}
return debugConfig;
}
};
suite('Go Debug Adapter Tests (legacy)', function () {
this.timeout(60_000);
testAll(this.ctx, false);
});
suite('Go Debug Adapter Tests (dlv-dap)', function () {
this.timeout(60_000);
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 {
static async create(config: DebugConfiguration) {
const d = new DelveDAPDebugAdapterOnSocket(config);
await d.startAndConnectToServer();
return d;
}
private constructor(config: DebugConfiguration) {
super(config, new TimestampedLogger('error', undefined, PRINT_TO_CONSOLE));
}
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(timeoutMS?: number) {
if (this._disposed) {
return;
}
this._disposed = true;
this.log('adapter disposing');
await this._server.close();
await super.dispose(timeoutMS);
this.log('adapter disposed');
}
// 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);
if (PRINT_TO_CONSOLE) {
console.log(msg);
}
}
public printLog() {
this._log.forEach((msg) => console.log(msg));
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}