blob: 8ef111c430196b3b9fb2b45fe3fcf495cb055411 [file] [log] [blame]
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable no-prototype-builtins */
import assert = require('assert');
import fs = require('fs');
import os = require('os');
import path = require('path');
import sinon = require('sinon');
import vscode = require('vscode');
import { getGoConfig } from '../../src/config';
import { GoDebugConfigurationProvider } from '../../src/goDebugConfiguration';
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
import { rmdirRecursive } from '../../src/util';
import goEnv = require('../../src/goEnv');
import { isInPreviewMode } from '../../src/goLanguageServer';
import { MockCfg } from '../mocks/MockCfg';
suite('Debug Environment Variable Merge Test', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
suiteSetup(async () => {
await updateGoVarsFromConfig();
// Set up the test fixtures.
const fixtureSourcePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata');
const filePath = path.join(fixtureSourcePath, 'baseTest', 'test.go');
await vscode.workspace.openTextDocument(vscode.Uri.file(filePath));
});
suiteTeardown(() => {
vscode.commands.executeCommand('workbench.action.closeActiveEditor');
});
let sandbox: sinon.SinonSandbox;
let tmpDir = '';
const toolExecutionEnv: NodeJS.Dict<string> = {};
setup(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'godebugconfig_test'));
sandbox = sinon.createSandbox();
});
teardown(() => {
sandbox.restore();
rmdirRecursive(tmpDir);
});
interface Input {
env?: { [key: string]: any };
envFile?: string | string[];
toolsEnv?: { [key: string]: any };
}
function runTest(input: Input, expected: { [key: string]: any }) {
sandbox.stub(goEnv, 'toolExecutionEnvironment').returns(input.toolsEnv || {});
const config = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, {
type: 'go',
name: 'Launch',
request: 'launch',
env: input.env,
envFile: input.envFile
});
const actual = config.env;
assert.deepStrictEqual(actual, expected);
}
test('works with empty launchArgs', () => {
runTest({}, {});
});
test('toolsEnvVars is propagated', () => {
const toolsEnv = {
GOPATH: '/gopath',
GOOS: 'valueFromToolsEnv'
};
runTest(
{ toolsEnv },
{
GOPATH: '/gopath',
GOOS: 'valueFromToolsEnv'
}
);
});
test('preserves settings from launchArgs.env', () => {
const env = { GOPATH: 'valueFromEnv', GOOS: 'valueFromEnv2' };
runTest(
{ env },
{
GOPATH: 'valueFromEnv',
GOOS: 'valueFromEnv2'
}
);
});
test('preserves settings from launchArgs.envFile', () => {
const envFile = path.join(tmpDir, 'env');
fs.writeFileSync(envFile, 'GOPATH=valueFromEnvFile');
runTest({ envFile }, { GOPATH: 'valueFromEnvFile' });
});
test('launchArgs.env overwrites launchArgs.envFile', () => {
const env = { SOMEVAR1: 'valueFromEnv' };
const envFile = path.join(tmpDir, 'env');
fs.writeFileSync(envFile, ['SOMEVAR1=valueFromEnvFile1', 'SOMEVAR2=valueFromEnvFile2'].join('\n'));
runTest(
{ env, envFile },
{
SOMEVAR1: 'valueFromEnv',
SOMEVAR2: 'valueFromEnvFile2'
}
);
});
test('launchArgs.env overwrites toolsEnvVar', () => {
const toolsEnv = {
GOPATH: '/gopath',
SOMEVAR1: 'valueFromToolsEnvVar1',
SOMEVAR2: 'valueFromToolsEnvVar2'
};
const env = { SOMEVAR1: 'valueFromEnv' };
runTest(
{ env, toolsEnv },
{
GOPATH: '/gopath',
SOMEVAR1: 'valueFromEnv',
SOMEVAR2: 'valueFromToolsEnvVar2'
}
);
});
test('launchArgs.envFile overwrites toolsEnvVar', () => {
const toolsEnv = {
GOPATH: '/gopath',
SOMEVAR1: 'valueFromToolsEnvVar1',
SOMEVAR2: 'valueFromToolsEnvVar2'
};
const envFile = path.join(tmpDir, 'env');
fs.writeFileSync(envFile, ['SOMEVAR2=valueFromEnvFile2'].join('\n'));
runTest(
{ toolsEnv, envFile },
{
GOPATH: '/gopath',
SOMEVAR1: 'valueFromToolsEnvVar1',
SOMEVAR2: 'valueFromEnvFile2'
}
);
});
});
suite('Debug Configuration Merge User Settings', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
const config = require('../../src/config');
teardown(() => sinon.restore());
suite("merge 'go' config from settings.json", () => {
test('go flags config does not affect debug config', async () => {
// This tests that the testFlags and GOOS and GOARCH set
// in settings.json do not affect the resolved debug configuration.
// When this expected behavior changes, this test can be updated.
// Run resolveDebugConfiguration with the default workspace settings.
const cfg1 = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}'
};
const emptyResult = await debugConfigProvider.resolveDebugConfiguration(undefined, cfg1);
const goConfig = Object.create(getGoConfig(), {
testFlags: { value: '-tags=myTagTest' },
buildFlags: { value: '-tags=myTagBuild' },
goroot: { value: '/path/to/goroot' },
gopath: { value: '/path/to/gopath' }
}) as vscode.WorkspaceConfiguration;
// Adjust the workspace config.
sinon.stub(config, 'getGoConfig').returns(goConfig);
const cfg2 = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}'
};
const filledResult = await debugConfigProvider.resolveDebugConfiguration(undefined, cfg2);
assert.strictEqual(filledResult.name, emptyResult.name);
assert.strictEqual(filledResult.type, emptyResult.type);
assert.strictEqual(filledResult.mode, emptyResult.mode);
assert.strictEqual(filledResult.request, emptyResult.request);
assert.strictEqual(filledResult.program, emptyResult.program);
assert.strictEqual(filledResult.dlvToolPath, emptyResult.dlvToolPath);
assert.strictEqual(filledResult.apiVersion, emptyResult.apiVersion);
assert.strictEqual(filledResult.showGlobalVariables, emptyResult.showGlobalVariables);
assert.strictEqual(filledResult.debugAdapter, emptyResult.debugAdapter);
assert.strictEqual(filledResult.substitutePath, emptyResult.substitutePath);
});
test('delve config in settings.json is added to debug config', async () => {
// This tests that the testFlags and GOOS and GOARCH set
// in settings.json do not affect the resolved debug configuration.
// When this expected behavior changes, this test can be updated.
// Run resolveDebugConfiguration with the default workspace settings.
const goConfig = new MockCfg({
delveConfig: {
dlvLoadConfig: {
followPointers: false,
maxVariableRecurse: 3,
maxStringLen: 32,
maxArrayValues: 32,
maxStructFields: 5
},
apiVersion: 1,
showGlobalVariables: true,
debugAdapter: 'dlv-dap',
substitutePath: [{ from: 'hello', to: 'goodbye' }]
}
});
sinon.stub(config, 'getGoConfig').returns(goConfig);
const cfg = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}'
};
const result = await debugConfigProvider.resolveDebugConfiguration(undefined, cfg);
assert.strictEqual(result.apiVersion, 1);
assert.strictEqual(result.showGlobalVariables, true);
assert.strictEqual(result.debugAdapter, 'dlv-dap');
assert.strictEqual(result.substitutePath.length, 1);
assert.strictEqual(result.substitutePath[0].from, 'hello');
assert.strictEqual(result.substitutePath[0].to, 'goodbye');
const dlvLoadConfig = result.dlvLoadConfig;
assert.strictEqual(dlvLoadConfig.followPointers, false);
assert.strictEqual(dlvLoadConfig.maxVariableRecurse, 3);
assert.strictEqual(dlvLoadConfig.maxStringLen, 32);
assert.strictEqual(dlvLoadConfig.maxArrayValues, 32);
assert.strictEqual(dlvLoadConfig.maxStructFields, 5);
});
test('delve config in settings.json is overriden by launch.json', async () => {
// This tests that the testFlags and GOOS and GOARCH set
// in settings.json do not affect the resolved debug configuration.
// When this expected behavior changes, this test can be updated.
// Run resolveDebugConfiguration with the default workspace settings.
const goConfig = new MockCfg({
delveConfig: {
dlvLoadConfig: {
followPointers: false,
maxVariableRecurse: 3,
maxStringLen: 32,
maxArrayValues: 32,
maxStructFields: 5
},
apiVersion: 1,
showGlobalVariables: true,
debugAdapter: 'dlv-dap',
substitutePath: [{ from: 'hello', to: 'goodbye' }]
}
});
sinon.stub(config, 'getGoConfig').returns(goConfig);
const cfg: vscode.DebugConfiguration = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}',
apiVersion: 2,
showGlobalVariables: false,
dlvLoadConfig: {
followPointers: true,
maxVariableRecurse: 6,
maxStringLen: 128,
maxArrayValues: 128,
maxStructFields: -1
},
debugAdapter: 'legacy',
substitutePath: []
};
const result = await debugConfigProvider.resolveDebugConfiguration(undefined, cfg);
assert.strictEqual(result.apiVersion, 2);
assert.strictEqual(result.showGlobalVariables, false);
assert.strictEqual(result.debugAdapter, 'legacy');
assert.strictEqual(result.substitutePath.length, 0);
const dlvLoadConfig = result.dlvLoadConfig;
assert.strictEqual(dlvLoadConfig.followPointers, true);
assert.strictEqual(dlvLoadConfig.maxVariableRecurse, 6);
assert.strictEqual(dlvLoadConfig.maxStringLen, 128);
assert.strictEqual(dlvLoadConfig.maxArrayValues, 128);
assert.strictEqual(dlvLoadConfig.maxStructFields, -1);
});
});
});
suite('Debug Configuration Modify User Config', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
suite('remove gcflags', () => {
test('remove gcflags from string args', () => {
const tt = [
{
input: '-gcflags=all=-l',
want: { args: '', removed: true }
},
{
input: '-gcflags all=-l',
want: { args: '', removed: true }
},
// Preserve other flags
{
input: '-race -gcflags=all=-l -mod=mod',
want: { args: '-race -mod=mod', removed: true }
},
{
input: '-race -gcflags all=-l -mod=mod',
want: { args: '-race -mod=mod', removed: true }
},
// Test with quoted value
{
input: "-mod=mod -gcflags=test/...='hello goodbye' -race",
want: { args: '-mod=mod -race', removed: true }
},
{
input: '-mod=mod -gcflags test/...="hello goodbye" -race',
want: { args: '-mod=mod -race', removed: true }
},
{
input: "-mod=mod -gcflags='test/...=hello goodbye' -race",
want: { args: '-mod=mod -race', removed: true }
},
{
input: '-mod=mod -gcflags "test/...=hello goodbye" -race',
want: { args: '-mod=mod -race', removed: true }
},
// Multiple -gcflags present
{
input: '-mod=mod -gcflags "test/...=hello goodbye" -race -gcflags=all="hello goodbye"',
want: { args: '-mod=mod -race', removed: true }
},
// No gcflags are present
{
input: '',
want: { args: '', removed: false }
},
{
input: '-race -mod=gcflags',
want: { args: '-race -mod=gcflags', removed: false }
}
];
tt.forEach((tc) => {
const got = debugConfigProvider.removeGcflags(tc.input);
assert.strictEqual(got.args, tc.want.args, `args for ${tc.input} do not match expected`);
assert.strictEqual(got.removed, tc.want.removed, `removed for ${tc.input} does not match expected`);
});
});
test('remove user set -gcflags in buildFlags', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}',
env: {},
buildFlags: '-race -gcflags=-l -mod=mod'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.strictEqual(config.buildFlags, '-race -mod=mod');
});
test('remove user set -gcflags in GOFLAGS', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}',
env: { GOFLAGS: '-race -gcflags=-l -mod=mod' }
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.strictEqual(config.env.GOFLAGS, '-race -mod=mod');
});
});
});
suite('Debug Configuration Resolve Paths', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
test('resolve ~ in cwd', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}',
cwd: '~/main.go'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.notStrictEqual(config.cwd, '~/main.go');
});
test('do not resolve workspaceFolder or fileDirname', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '${fileDirname}',
cwd: '${workspaceFolder}'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.strictEqual(config.cwd, '${workspaceFolder}');
assert.strictEqual(config.program, '${fileDirname}');
});
});
suite('Debug Configuration Converts Relative Paths', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
function debugConfig(adapter: string) {
return {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
debugAdapter: adapter,
program: path.join('foo', 'bar.go'),
cwd: '.',
output: 'debug'
};
}
test('resolve relative paths with workspace root in dlv-dap mode, exec mode does not set __buildDir', () => {
const config = debugConfig('dlv-dap');
config.mode = 'exec';
config.program = path.join('foo', 'bar.exe');
const workspaceFolder = {
uri: vscode.Uri.file(os.tmpdir()),
name: 'test',
index: 0
};
const { program, cwd, __buildDir } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
workspaceFolder,
config
);
assert.deepStrictEqual(
{ program, cwd, __buildDir },
{
program: path.join(os.tmpdir(), 'foo', 'bar.exe'),
cwd: os.tmpdir(),
__buildDir: undefined
}
);
});
test('program and __buildDir are updated while resolving debug configuration in dlv-dap mode', () => {
const config = debugConfig('dlv-dap');
config.program = path.join('foo', 'bar', 'pkg');
const workspaceFolder = {
uri: vscode.Uri.file(os.tmpdir()),
name: 'test',
index: 0
};
const {
program,
cwd,
output,
__buildDir
} = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(workspaceFolder, config);
assert.deepStrictEqual(
{ program, cwd, output, __buildDir },
{
program: '.',
cwd: os.tmpdir(),
output: path.join(os.tmpdir(), 'debug'),
__buildDir: path.join(os.tmpdir(), 'foo', 'bar', 'pkg')
}
);
});
test('empty, undefined paths are not affected', () => {
const config = debugConfig('dlv-dap');
config.program = undefined;
config.cwd = '';
delete config.output;
const workspaceFolder = {
uri: vscode.Uri.file(os.tmpdir()),
name: 'test',
index: 0
};
const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
workspaceFolder,
config
);
assert.deepStrictEqual(
{ program, cwd, output },
{
program: undefined,
cwd: '',
output: undefined
}
);
});
test('relative paths with no workspace root are not expanded', () => {
const config = debugConfig('dlv-dap');
const {
program,
cwd,
output,
__buildDir
} = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(undefined, config);
assert.deepStrictEqual(
{ program, cwd, output, __buildDir },
{
program: '.' + path.sep + 'bar.go',
cwd: '.',
output: 'debug',
__buildDir: 'foo'
}
);
});
test('do not affect relative paths (workspace) in legacy mode', () => {
const config = debugConfig('legacy');
const workspaceFolder = {
uri: vscode.Uri.file(os.tmpdir()),
name: 'test',
index: 0
};
const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
workspaceFolder,
config
);
assert.deepStrictEqual(
{ program, cwd, output },
{
program: path.join('foo', 'bar.go'),
cwd: '.',
output: 'debug'
}
);
});
test('do not affect relative paths (no workspace) in legacy mode', () => {
const config = debugConfig('legacy');
const { program, cwd, output } = debugConfigProvider.resolveDebugConfigurationWithSubstitutedVariables(
undefined,
config
);
assert.deepStrictEqual(
{ program, cwd, output },
{
program: path.join('foo', 'bar.go'),
cwd: '.',
output: 'debug'
}
);
});
});
suite('Debug Configuration Auto Mode', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
test('resolve auto to debug with non-test file', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '/path/to/main.go'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.strictEqual(config.mode, 'debug');
assert.strictEqual(config.program, '/path/to/main.go');
});
test('resolve auto to debug with test file', () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '/path/to/main_test.go'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
assert.strictEqual(config.mode, 'test');
assert.strictEqual(config.program, '/path/to');
});
});
suite('Debug Configuration Default DebugAdapter', () => {
const debugConfigProvider = new GoDebugConfigurationProvider();
test("default debugAdapter should be 'dlv-dap'", () => {
const config = {
name: 'Launch',
type: 'go',
request: 'launch',
mode: 'auto',
program: '/path/to/main.go'
};
debugConfigProvider.resolveDebugConfiguration(undefined, config);
const resolvedConfig = config as any;
assert.strictEqual(resolvedConfig['debugAdapter'], 'dlv-dap');
});
test("default debugAdapter for remote mode should be always 'legacy'", () => {
const config = {
name: 'Attach',
type: 'go',
request: 'attach',
mode: 'remote',
program: '/path/to/main_test.go',
cwd: '/path'
};
const want = 'legacy'; // remote mode works only with legacy mode.
debugConfigProvider.resolveDebugConfiguration(undefined, config);
const resolvedConfig = config as any;
assert.strictEqual(resolvedConfig['debugAdapter'], want);
});
test('debugAdapter=dlv-dap should be ignored for remote mode', () => {
const config = {
name: 'Attach',
type: 'go',
request: 'attach',
mode: 'remote',
debugAdapter: 'dlv-dap',
program: '/path/to/main_test.go',
cwd: '/path'
};
const want = 'legacy'; // remote mode works only with legacy mode.
debugConfigProvider.resolveDebugConfiguration(undefined, config);
const resolvedConfig = config as any;
assert.strictEqual(resolvedConfig['debugAdapter'], want);
});
});