Skip to content

Commit

Permalink
test/gopls/codelens: move codelense test to gopls-based test
Browse files Browse the repository at this point in the history
Instead of using the legacy document symbol provider, this test
will use the gopls-based go document symbol provider to find
the test function code lens places.

goplsTestEnv.utils.ts is a collection of helpers that starts the gopls
and collects the logs. They have been used by tests in
test/gopls/extension.test.ts. Now we are moving to a separate file
so other tests can use them to interact with gopls.

For #2799
For #1020

Change-Id: Ib3582073960db67e1f1ec5b284ab4945258cb62a
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/535255
Commit-Queue: Hyang-Ah Hana Kim <[email protected]>
TryBot-Result: kokoro <[email protected]>
Reviewed-by: Suzy Mueller <[email protected]>
  • Loading branch information
hyangah committed Oct 24, 2023
1 parent da12e36 commit dcf5ecc
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 178 deletions.
79 changes: 34 additions & 45 deletions test/integration/codelens.test.ts → test/gopls/codelens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,113 +6,101 @@
'use strict';

import assert from 'assert';
import fs = require('fs-extra');
import path = require('path');
import sinon = require('sinon');
import vscode = require('vscode');
import { updateGoVarsFromConfig } from '../../src/goInstallTools';
import { GoRunTestCodeLensProvider } from '../../src/goRunTestCodelens';
import { subTestAtCursor } from '../../src/goTest';
import { getCurrentGoPath, getGoVersion } from '../../src/util';
import { MockExtensionContext } from '../mocks/MockContext';
import { Env } from './goplsTestEnv.utils';

suite('Code lenses for testing and benchmarking', function () {
this.timeout(20000);

let gopath: string;
let repoPath: string;
let fixturePath: string;
let fixtureSourcePath: string;

let document: vscode.TextDocument;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ctx = new MockExtensionContext() as any;
const cancellationTokenSource = new vscode.CancellationTokenSource();
const codeLensProvider = new GoRunTestCodeLensProvider({});

const projectDir = path.join(__dirname, '..', '..', '..');
const testdataDir = path.join(projectDir, 'test', 'testdata', 'codelens');
const env = new Env();

this.afterEach(async function () {
// 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');
sinon.restore();
});

suiteSetup(async () => {
await updateGoVarsFromConfig({});

gopath = getCurrentGoPath();
if (!gopath) {
assert.fail('Cannot run tests without a configured GOPATH');
}
console.log(`Using GOPATH: ${gopath}`);

// Set up the test fixtures.
repoPath = path.join(gopath, 'src', 'test');
fixturePath = path.join(repoPath, 'testfixture');
fixtureSourcePath = path.join(__dirname, '..', '..', '..', 'test', 'testdata', 'codelens');

fs.removeSync(repoPath);
fs.copySync(fixtureSourcePath, fixturePath, {
recursive: true
});
const uri = vscode.Uri.file(path.join(fixturePath, 'codelens_test.go'));
const uri = vscode.Uri.file(path.join(testdataDir, 'codelens_test.go'));
await env.startGopls(uri.fsPath);
document = await vscode.workspace.openTextDocument(uri);
});

suiteTeardown(() => {
fs.removeSync(repoPath);
});

teardown(() => {
sinon.restore();
suiteTeardown(async () => {
await env.teardown();
});

test('Subtests - runs a test with cursor on t.Run line', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(7, 4, 7, 4);
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, true);
});

test('Subtests - runs a test with cursor within t.Run function', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(8, 4, 8, 4);
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, true);
});

test('Subtests - returns false for a failing test', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(11, 4, 11, 4);
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, false);
});

test('Subtests - does nothing for a dynamically defined subtest', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(17, 4, 17, 4);
sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves(undefined);
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, undefined);
});

test('Subtests - runs a test with curson on t.Run line and dynamic test name is passed in input box', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(17, 4, 17, 4);
sinon.stub(vscode.window, 'showInputBox').onFirstCall().resolves('dynamic test name');
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, false);
});

test('Subtests - does nothing when cursor outside of a test function', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(5, 0, 5, 0);
const result = await subTestAtCursor('test')(ctx, {})([]);
const result = await subTestAtCursor('test')(ctx, env.goCtx)([]);
assert.equal(result, undefined);
});

test('Subtests - does nothing when no test function covers the cursor and a function name is passed in', async () => {
const editor = await vscode.window.showTextDocument(document);
editor.selection = new vscode.Selection(5, 0, 5, 0);
const result = await subTestAtCursor('test')(ctx, {})({ functionName: 'TestMyFunction' });
const result = await subTestAtCursor('test')(ctx, env.goCtx)({ functionName: 'TestMyFunction' });
assert.equal(result, undefined);
});

test('Test codelenses', async () => {
const codeLensProvider = new GoRunTestCodeLensProvider(env.goCtx);
const codeLenses = await codeLensProvider.provideCodeLenses(document, cancellationTokenSource.token);
assert.equal(codeLenses.length, 8);
const wantCommands = [
Expand All @@ -131,7 +119,8 @@ suite('Code lenses for testing and benchmarking', function () {
});

test('Benchmark codelenses', async () => {
const uri = vscode.Uri.file(path.join(fixturePath, 'codelens_benchmark_test.go'));
const codeLensProvider = new GoRunTestCodeLensProvider(env.goCtx);
const uri = vscode.Uri.file(path.join(testdataDir, 'codelens_benchmark_test.go'));
const benchmarkDocument = await vscode.workspace.openTextDocument(uri);
const codeLenses = await codeLensProvider.provideCodeLenses(benchmarkDocument, cancellationTokenSource.token);
assert.equal(codeLenses.length, 6);
Expand All @@ -149,7 +138,8 @@ suite('Code lenses for testing and benchmarking', function () {
});

test('Test codelenses include only valid test function names', async () => {
const uri = vscode.Uri.file(path.join(fixturePath, 'codelens2_test.go'));
const codeLensProvider = new GoRunTestCodeLensProvider(env.goCtx);
const uri = vscode.Uri.file(path.join(testdataDir, 'codelens2_test.go'));
const benchmarkDocument = await vscode.workspace.openTextDocument(uri);
const codeLenses = await codeLensProvider.provideCodeLenses(benchmarkDocument, cancellationTokenSource.token);
assert.equal(codeLenses.length, 20, JSON.stringify(codeLenses, null, 2));
Expand All @@ -175,11 +165,9 @@ suite('Code lenses for testing and benchmarking', function () {
]);
});

test('Test codelenses include valid fuzz function names', async function () {
if ((await getGoVersion()).lt('1.18')) {
this.skip();
}
const uri = vscode.Uri.file(path.join(fixturePath, 'codelens_go118_test.go'));
test('Test codelenses include valid fuzz function names', async () => {
const codeLensProvider = new GoRunTestCodeLensProvider(env.goCtx);
const uri = vscode.Uri.file(path.join(testdataDir, 'codelens_go118_test.go'));
const testDocument = await vscode.workspace.openTextDocument(uri);
const codeLenses = await codeLensProvider.provideCodeLenses(testDocument, cancellationTokenSource.token);
assert.equal(codeLenses.length, 8, JSON.stringify(codeLenses, null, 2));
Expand All @@ -196,7 +184,8 @@ suite('Code lenses for testing and benchmarking', function () {
});

test('Test codelenses skip TestMain', async () => {
const uri = vscode.Uri.file(path.join(fixturePath, 'testmain/testmain_test.go'));
const codeLensProvider = new GoRunTestCodeLensProvider(env.goCtx);
const uri = vscode.Uri.file(path.join(testdataDir, 'testmain/testmain_test.go'));
const testDocument = await vscode.workspace.openTextDocument(uri);
const codeLenses = await codeLensProvider.provideCodeLenses(testDocument, cancellationTokenSource.token);
assert.equal(codeLenses.length, 4, JSON.stringify(codeLenses, null, 2));
Expand Down
134 changes: 1 addition & 133 deletions test/gopls/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,146 +4,14 @@
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import assert from 'assert';
import { EventEmitter } from 'events';
import * as path from 'path';
import * as vscode from 'vscode';
import { LanguageClient } from 'vscode-languageclient/node';
import { getGoConfig } from '../../src/config';
import {
buildLanguageClient,
BuildLanguageClientOption,
buildLanguageServerConfig
} from '../../src/language/goLanguageServer';
import sinon = require('sinon');
import { getGoVersion, GoVersion } from '../../src/util';
import { GOPLS_MAYBE_PROMPT_FOR_TELEMETRY, TELEMETRY_START_TIME_KEY, TelemetryService } from '../../src/goTelemetry';
import { MockMemento } from '../mocks/MockMemento';

// FakeOutputChannel is a fake output channel used to buffer
// the output of the tested language client in an in-memory
// string array until cleared.
class FakeOutputChannel implements vscode.OutputChannel {
public name = 'FakeOutputChannel';
public show = sinon.fake(); // no-empty
public hide = sinon.fake(); // no-empty
public dispose = sinon.fake(); // no-empty
public replace = sinon.fake(); // no-empty

private buf = [] as string[];

private eventEmitter = new EventEmitter();
private registeredPatterns = new Set<string>();
public onPattern(msg: string, listener: () => void) {
this.registeredPatterns.add(msg);
this.eventEmitter.once(msg, () => {
this.registeredPatterns.delete(msg);
listener();
});
}

public append = (v: string) => this.enqueue(v);
public appendLine = (v: string) => this.enqueue(v);
public clear = () => {
this.buf = [];
};
public toString = () => {
return this.buf.join('\n');
};

private enqueue = (v: string) => {
this.registeredPatterns?.forEach((p) => {
if (v.includes(p)) {
this.eventEmitter.emit(p);
}
});

if (this.buf.length > 1024) {
this.buf.shift();
}
this.buf.push(v.trim());
};
}

// Env is a collection of test-related variables and lsp client.
// Currently, this works only in module-aware mode.
class Env {
public languageClient?: LanguageClient;
private fakeOutputChannel?: FakeOutputChannel;
private disposables = [] as { dispose(): any }[];

public flushTrace(print: boolean) {
if (print) {
console.log(this.fakeOutputChannel?.toString());
}
this.fakeOutputChannel?.clear();
}

// This is a hack to check the progress of package loading.
// TODO(hyangah): use progress message middleware hook instead
// once it becomes available.
public onMessageInTrace(msg: string, timeoutMS: number): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.flushTrace(true);
reject(`Timed out while waiting for '${msg}'`);
}, timeoutMS);
this.fakeOutputChannel?.onPattern(msg, () => {
clearTimeout(timeout);
resolve();
});
});
}

// Start the language server with the fakeOutputChannel.
public async startGopls(filePath: string, goConfig?: vscode.WorkspaceConfiguration) {
// file path to open.
this.fakeOutputChannel = new FakeOutputChannel();
const pkgLoadingDone = this.onMessageInTrace('Finished loading packages.', 60_000);

if (!goConfig) {
goConfig = getGoConfig();
}
const cfg: BuildLanguageClientOption = buildLanguageServerConfig(
Object.create(goConfig, {
useLanguageServer: { value: true },
languageServerFlags: { value: ['-rpc.trace'] } // enable rpc tracing to monitor progress reports
})
);
cfg.outputChannel = this.fakeOutputChannel; // inject our fake output channel.
this.languageClient = await buildLanguageClient({}, cfg);
if (!this.languageClient) {
throw new Error('Language client not initialized.');
}

await this.languageClient.start();
await this.openDoc(filePath);
await pkgLoadingDone;
}

public async teardown() {
try {
await vscode.commands.executeCommand('workbench.action.closeActiveEditor');
await this.languageClient?.stop(1_000); // 1s timeout
} catch (e) {
console.log(`failed to stop gopls within 1sec: ${e}`);
} finally {
if (this.languageClient?.isRunning()) {
console.log(`failed to stop language client on time: ${this.languageClient?.state}`);
this.flushTrace(true);
}
for (const d of this.disposables) {
d.dispose();
}
this.languageClient = undefined;
}
}

public async openDoc(...paths: string[]) {
const uri = vscode.Uri.file(path.resolve(...paths));
const doc = await vscode.workspace.openTextDocument(uri);
return { uri, doc };
}
}
import { Env } from './goplsTestEnv.utils';

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
Expand Down
Loading

0 comments on commit dcf5ecc

Please sign in to comment.