blob: 69d1eb5b1c31268ac6025dee450a61c57f758ff8 [file] [log] [blame]
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable node/no-unpublished-import */
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';
import { getGoConfig } from '../../src/config';
import sinon = require('sinon');
import { getGoVersion, GoVersion } from '../../src/util';
import { GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, recordTelemetryStartTime, TelemetryService } from '../../src/goTelemetry';
import { MockMemento } from '../mocks/MockMemento';
import { Env } from './goplsTestEnv.utils';
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
suite('Go Extension Tests With Gopls', function () {
this.timeout(300000);
const projectDir = path.join(__dirname, '..', '..', '..');
const testdataDir = path.join(projectDir, 'test', 'testdata');
const env = new Env();
const sandbox = sinon.createSandbox();
let goVersion: GoVersion;
suiteSetup(async () => {
goVersion = await getGoVersion();
});
this.afterEach(async function () {
await env.teardown();
// Note: this shouldn't use () => {...}. Arrow functions do not have 'this'.
// I don't know why but this.currentTest.state does not have the expected value when
// used with teardown.
env.flushTrace(this.currentTest?.state === 'failed');
sandbox.restore();
});
test('HoverProvider', async () => {
const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
const testCases: [string, vscode.Position, string | null, string | null][] = [
// [new vscode.Position(3,3), '/usr/local/go/src/fmt'],
['keyword', new vscode.Position(0, 3), null, null], // keyword
['inside a string', new vscode.Position(23, 14), null, null], // inside a string
['just a }', new vscode.Position(20, 0), null, null], // just a }
['inside a number', new vscode.Position(28, 16), null, null], // inside a number
['func main()', new vscode.Position(22, 5), 'func main()', null],
['import "math"', new vscode.Position(40, 23), 'package math', '`math` on'],
[
'func Println()',
new vscode.Position(19, 6),
goVersion.lt('1.18')
? 'func fmt.Println(a ...interface{}) (n int, err error)'
: 'func fmt.Println(a ...any) (n int, err error)',
'Println formats '
],
['func print()', new vscode.Position(23, 4), 'func print(txt string)', 'This is an unexported function ']
];
const promises = testCases.map(async ([name, position, expectedSignature, expectedDoc]) => {
const hovers = (await vscode.commands.executeCommand(
'vscode.executeHoverProvider',
uri,
position
)) as vscode.Hover[];
if (expectedSignature === null && expectedDoc === null) {
assert.equal(hovers.length, 0, `check hovers over ${name} failed: unexpected non-empty hover message.`);
return;
}
const hover = hovers[0];
assert.equal(
hover.contents.length,
1,
`check hovers over ${name} failed: unexpected number of hover messages.`
);
const gotMessage = (<vscode.MarkdownString>hover.contents[0]).value;
assert.ok(
gotMessage.includes('```go\n' + expectedSignature + '\n```') &&
(!expectedDoc || gotMessage.includes(expectedDoc)),
`check hovers over ${name} failed: got ${gotMessage}`
);
});
return Promise.all(promises);
});
test('Completion middleware', async () => {
const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
const { uri } = await env.openDoc(testdataDir, 'gogetdocTestData', 'test.go');
const testCases: [string, vscode.Position, string, vscode.CompletionItemKind][] = [
['fmt.P<>', new vscode.Position(19, 6), 'Print', vscode.CompletionItemKind.Function],
['xyz.H<>', new vscode.Position(41, 13), 'Hello', vscode.CompletionItemKind.Method]
];
for (const [name, position, wantFilterText, wantItemKind] of testCases) {
let list: vscode.CompletionList<vscode.CompletionItem> | undefined;
// Query completion items. We expect the hard coded filter text hack
// has been applied and gopls returns an incomplete list by default
// to avoid reordering by vscode. But, if the query is made before
// gopls is ready, we observed that gopls returns an empty result
// as a complete result, and vscode returns a general completion list instead.
// Retry a couple of times if we see a complete result as a workaround.
// (github.com/golang/vscode-go/issues/363)
for (let i = 0; i < 3; i++) {
list = (await vscode.commands.executeCommand(
'vscode.executeCompletionItemProvider',
uri,
position
)) as vscode.CompletionList;
if (list.isIncomplete) {
break;
}
await sleep(100);
console.log(`${new Date()}: retrying...`);
}
// Confirm that the hardcoded filter text hack has been applied.
if (!list || !list.isIncomplete) {
assert.fail('gopls should provide an incomplete list by default');
}
// vscode.executeCompletionItemProvider will return results from all
// registered completion item providers, not only gopls but also snippets.
// Alternative is to directly query the language client, but that will
// prevent us from detecting problems caused by issues between the language
// client library and the vscode.
let itemKindFound = false;
for (const item of list.items) {
if (item.kind === vscode.CompletionItemKind.Snippet) {
continue;
} // gopls does not supply Snippet yet.
assert.strictEqual(
item.filterText ?? item.label,
wantFilterText,
`${uri}:${name} failed, unexpected filter text ` +
`(got ${item.filterText ?? item.label}, want ${wantFilterText})\n` +
`${JSON.stringify(item, null, 2)}`
);
if (item.kind === wantItemKind) {
itemKindFound = true;
}
if (
item.kind === vscode.CompletionItemKind.Method ||
item.kind === vscode.CompletionItemKind.Function
) {
assert.ok(
item.command,
`${uri}:${name}: expected command associated with ${item.label}, found none`
);
}
}
assert(itemKindFound, `failed to find expected item kind ${wantItemKind}: got ${JSON.stringify(list)}`);
}
});
async function testCustomFormatter(goConfig: vscode.WorkspaceConfiguration, customFormatter: string) {
const config = require('../../src/config');
sandbox.stub(config, 'getGoConfig').returns(goConfig);
const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
await env.startGopls(path.join(workspaceDir, 'test.go'), goConfig, workspaceDir);
const { doc } = await env.openDoc(testdataDir, 'gogetdocTestData', 'format.go');
await vscode.window.showTextDocument(doc);
const formatFeature = env.languageClient?.getFeature('textDocument/formatting');
const formatter = formatFeature?.getProvider(doc);
const tokensrc = new vscode.CancellationTokenSource();
try {
const result = await formatter?.provideDocumentFormattingEdits(
doc,
{} as vscode.FormattingOptions,
tokensrc.token
);
assert.fail(`formatter unexpectedly succeeded and returned a result: ${JSON.stringify(result)}`);
} catch (e) {
assert(`${e}`.includes(`errors when formatting with ${customFormatter}`), `${e}`);
}
}
test('Nonexistent formatter', async () => {
const customFormatter = 'nonexistent';
const goConfig = Object.create(getGoConfig(), {
formatTool: { value: customFormatter } // this should make the formatter fail.
}) as vscode.WorkspaceConfiguration;
await testCustomFormatter(goConfig, customFormatter);
});
test('Custom formatter', async () => {
const customFormatter = 'coolCustomFormatter';
const goConfig = Object.create(getGoConfig(), {
formatTool: { value: 'custom' }, // this should make the formatter fail.
alternateTools: { value: { customFormatter: customFormatter } } // this should make the formatter fail.
}) as vscode.WorkspaceConfiguration;
await testCustomFormatter(goConfig, customFormatter);
});
test('Prompt For telemetry', async () => {
const workspaceDir = path.resolve(testdataDir, 'gogetdocTestData');
await env.startGopls(path.join(workspaceDir, 'test.go'), undefined, workspaceDir);
const memento = new MockMemento();
recordTelemetryStartTime(memento, new Date('2000-01-01'));
const sut = new TelemetryService(env.languageClient, memento, [GOPLS_MAYBE_PROMPT_FOR_TELEMETRY]);
try {
await Promise.all([
// we want to see the prompt command flowing.
env.onMessageInTrace(GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, 60_000),
sut.promptForTelemetry(
false /* not a preview */,
true /* vscode telemetry not disabled */,
1000 /* 1000 out of 1000 users */
)
]);
} catch (e) {
assert(false, `unexpected failure: ${e}`);
}
});
});