Skip to content

Commit

Permalink
test/gopls: initial test setup for gopls and hover tests (microsoft#3157
Browse files Browse the repository at this point in the history
)

This CL includes only HoverProvider tests, that use the same scenario
as the hover provider tests with godoc and gogetdoc in
integration/extension.test.ts.

Gopls test setup is complicated because gopls currently does not
work in a single-file mode nor support mono repos with multiple
modules. VS Code in test environment does not support dynamic
registration of workspace folders during testing.  So, the
workspace folder needs to be specified before the extension
host launches (by vscode-test.runTests or launch.json extensionHost
type tasks).

As a workaround, I use an empty directory and start the extension
host with the empty directory for a testing. Then, let the test
populate the directory with necessary files. This is not ideal;
file copy is slow and the use of the single workspace directory
prevents parallelized testing.

Commiting an empty directory to use as a scratch space is not
ideal either, but no worse than what vscode dev in my opinion.
https://github.com/microsoft/vscode/blob/master/.vscode/launch.json

I also considered shelling out the extension host launch
after creating a temp directory and using it as a workspace directory.
That may work inside test/runTest.ts, but complicates debugging
when sarting the test using launch.json.

Also passes '--user-data-dir' flag to prevent the tests from
using my personal vscode settings and interfere tests.

Change-Id: I7b064441720da5c89833afab35e8273de808cfad
  • Loading branch information
hyangah committed Apr 27, 2020
1 parent 7a57e56 commit f3dd04b
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 5 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ install:
- go get -u -v github.com/zmb3/gogetdoc
- go get -u -v golang.org/x/lint/golint
- go get -u -v golang.org/x/tools/cmd/gorename
- GO111MODULE=on go get golang.org/x/tools/gopls

script:
- npm run lint
Expand Down
23 changes: 22 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/integration/index",
"--timeout",
"999999",
"999999"
],
"stopOnEntry": false,
"sourceMaps": true,
Expand All @@ -53,6 +53,27 @@
],
"preLaunchTask": "npm: watch"
},
{
"name": "Launch Extension Tests with Gopls",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
// the workspace path should be GOPATH
"args": [
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/gopls/index",
"--user-data-dir", "${workspaceFolder}/test/gopls/testfixtures/src/workspace",
"--timeout", "999999",
"${workspaceFolder}/test/gopls/testfixtures/src/workspace" // gopls requires a workspace to work with.
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: watch",
},
{
"type": "node",
"request": "launch",
Expand Down
135 changes: 135 additions & 0 deletions test/gopls/extension.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import * as assert from 'assert';
import cp = require('child_process');
import * as fs from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { updateGoPathGoRootFromConfig } from '../../src/goInstallTools';
import { extensionId } from '../../src/telemetry';
import { getCurrentGoPath } from '../../src/util';

// Env is a collection of test related variables
// that define the test environment such as vscode workspace.
class Env {

// Currently gopls requires a workspace and does not work in a single-file mode.
// Code in test environment does not support dynamically adding folders.
// tslint:disable-next-line:max-line-length
// https://github.com/microsoft/vscode/blob/890f62dfd9f3e70198931f788c5c332b3e8b7ad7/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts#L281
//
// So, when we start the gopls tests, we start the test extension host with a
// dummy workspace, ${projectDir}/test/gopls/testfixtures/src/workspace
// (see test/runTest.ts and launch.json).
// Then copy necessary files to the workspace using Env.reset() from the
// fixturesRoot directory.
public workspaceDir: string;
public fixturesRoot: string;

public extension: vscode.Extension<any>;

constructor(projectDir: string) {
if (!projectDir) {
assert.fail('project directory cannot be determined');
}
this.workspaceDir = path.resolve(projectDir, 'test/gopls/testfixtures/src/workspace');
this.fixturesRoot = path.resolve(projectDir, 'test/fixtures');
this.extension = vscode.extensions.getExtension(extensionId);

// Ensure the vscode extension host is configured as expected.
const workspaceFolder = path.resolve(vscode.workspace.workspaceFolders[0].uri.fsPath);
if (this.workspaceDir !== workspaceFolder) {
assert.fail(`specified workspaceDir: ${this.workspaceDir} does not match the workspace folder: ${workspaceFolder}`);
}
}

public async setup() {
const wscfg = vscode.workspace.getConfiguration('go');
if (!wscfg.get('useLanguageServer')) {
wscfg.update('useLanguageServer', true, vscode.ConfigurationTarget.Workspace);
}

await this.reset();
await this.extension.activate();
await sleep(2000); // allow extension host + gopls to start.
}

public async reset(fixtureDirName?: string) { // name of the fixtures subdirectory to use.
try {
// clean everything except the .gitignore file
// needed to keep the empty directory in vcs.
await fs.readdir(this.workspaceDir).then((files) => {
return Promise.all(
files.filter((filename) => filename !== '.gitignore').map((file) => {
fs.remove(path.resolve(this.workspaceDir, file));
}));
});

if (!fixtureDirName) {
return;
}
const src = path.resolve(this.fixturesRoot, fixtureDirName);
const dst = this.workspaceDir;
await fs.copy(src, dst, { recursive: true });
} catch (err) {
assert.fail(err);
}
}

// openDoc opens the file in the workspace with the given path (paths
// are the path elements of a file).
public async openDoc(...paths: string[]) {
const uri = vscode.Uri.file(path.resolve(this.workspaceDir, ...paths));
const doc = await vscode.workspace.openTextDocument(uri);
return { uri, doc };
}
}

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

suite('Go Extension Tests With Gopls', function() {
this.timeout(1000000);
const projectDir = path.join(__dirname, '..', '..', '..');
const env = new Env(projectDir);

suiteSetup(async () => { await env.setup(); });
suiteTeardown(async () => { await env.reset(); });

test('HoverProvider', async () => {
await env.reset('gogetdocTestData');
const { uri, doc } = await env.openDoc('test.go');

// TODO(hyangah): find a way to wait for the language server to complete processing.

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), 'func fmt.Println(a ...interface{}) (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);
});
});
40 changes: 40 additions & 0 deletions test/gopls/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------*/
import * as glob from 'glob';
import * as Mocha from 'mocha';
import * as path from 'path';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd'
});
mocha.useColors(true);

const testsRoot = path.resolve(__dirname, '..');

return new Promise((c, e) => {
glob('gopls/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}

// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));

try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
e(err);
}
});
});
}
2 changes: 2 additions & 0 deletions test/gopls/testfixtures/src/workspace/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
31 changes: 27 additions & 4 deletions test/runTest.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
import * as fs from 'fs-extra';
import * as path from 'path';

import { runTests } from 'vscode-test';
import { extensionId } from '../src/telemetry';

async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
const extensionDevelopmentPath = path.resolve(__dirname, '../../');

try {
// The path to the extension test script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './integration/index');

// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests');
console.error('Failed to run integration tests' + err);
process.exit(1);
}

// Integration tests using gopls.
try {
// Currently gopls requires a workspace. Code in test environment does not support
// dynamically adding folders.
// tslint:disable-next-line:max-line-length
// https://github.com/microsoft/vscode/blob/890f62dfd9f3e70198931f788c5c332b3e8b7ad7/src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts#L281
// So, we start the test extension host with a dummy workspace (test/gopls/testfixtures/src/workspace)
// and copy necessary files to the workspace.
const ws = path.resolve(extensionDevelopmentPath, 'test/gopls/testfixtures/src/workspace');

await runTests({
extensionDevelopmentPath,
extensionTestsPath: path.resolve(__dirname, './gopls/index'),
launchArgs: [
'--disable-extensions', // disable all other extensions
ws // dummy workspace to start with
],
});
} catch (err) {
console.error('Failed to run gopls tests' + err);
}
}

main();

0 comments on commit f3dd04b

Please sign in to comment.