Skip to content

Commit 2020488

Browse files
authored
Merge pull request #481 from jneira/test-hls-log
Add integration smoke test
2 parents 28d7b2b + 453c98a commit 2020488

File tree

9 files changed

+152
-41
lines changed

9 files changed

+152
-41
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
build:
1111
strategy:
1212
matrix:
13-
os: [macos-latest, ubuntu-latest, windows-latest]
13+
os: [macos-11, ubuntu-latest, windows-latest]
1414
runs-on: ${{ matrix.os }}
1515
steps:
1616
- name: Checkout

.gitignore

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
out
2-
node_modules
3-
.vscode-test
4-
.DS_Store
5-
dist
1+
out
2+
node_modules
3+
.vscode-test
4+
test-workspace
5+
.DS_Store
6+
dist
67
*.vsix

package.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "haskell",
33
"displayName": "Haskell",
44
"description": "Haskell language support powered by the Haskell Language Server",
5-
"version": "1.7.1",
5+
"version": "1.7.2",
66
"license": "MIT",
77
"publisher": "haskell",
88
"engines": {
@@ -126,6 +126,12 @@
126126
"default": "",
127127
"description": "An optional URL to override where to check for haskell-language-server releases"
128128
},
129+
"haskell.releasesDownloadStoragePath": {
130+
"scope": "resource",
131+
"type": "string",
132+
"default": "",
133+
"markdownDescription": "An optional path where downloaded binaries will be stored. Check the default value [here](https://github.com/haskell/vscode-haskell#downloaded-binaries)"
134+
},
129135
"haskell.serverExecutablePath": {
130136
"scope": "resource",
131137
"type": "string",

src/extension.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
'use strict';
2-
import * as os from 'os';
32
import * as path from 'path';
43
import {
54
commands,
@@ -24,7 +23,7 @@ import { CommandNames } from './commands/constants';
2423
import { ImportIdentifier } from './commands/importIdentifier';
2524
import { DocsBrowser } from './docsBrowser';
2625
import { downloadHaskellLanguageServer } from './hlsBinaries';
27-
import { executableExists, ExtensionLogger } from './utils';
26+
import { executableExists, ExtensionLogger, resolvePathPlaceHolders } from './utils';
2827

2928
// The current map of documents & folders to language servers.
3029
// It may be null to indicate that we are in the process of launching a server,
@@ -103,12 +102,9 @@ function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder
103102
return null;
104103
}
105104
logger.info(`Trying to find the server executable in: ${exePath}`);
106-
// Substitute path variables with their corresponding locations.
107-
exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
108-
if (folder) {
109-
exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
110-
}
105+
exePath = resolvePathPlaceHolders(exePath, folder);
111106
logger.info(`Location after path variables subsitution: ${exePath}`);
107+
112108
if (!executableExists(exePath)) {
113109
throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and it is not on the PATH`);
114110
}

src/hlsBinaries.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as url from 'url';
77
import { promisify } from 'util';
88
import { env, ExtensionContext, ProgressLocation, Uri, window, workspace, WorkspaceFolder } from 'vscode';
99
import { Logger } from 'vscode-languageclient';
10-
import { downloadFile, executableExists, httpsGetSilently } from './utils';
10+
import { downloadFile, executableExists, httpsGetSilently, resolvePathPlaceHolders } from './utils';
1111
import * as validate from './validation';
1212

1313
/** GitHub API release */
@@ -102,7 +102,8 @@ async function getProjectGhcVersion(
102102
context: ExtensionContext,
103103
logger: Logger,
104104
dir: string,
105-
release: IRelease
105+
release: IRelease,
106+
storagePath: string
106107
): Promise<string> {
107108
const title: string = 'Working out the project GHC version. This might take a while...';
108109
logger.info(title);
@@ -174,7 +175,7 @@ async function getProjectGhcVersion(
174175
// Otherwise search to see if we previously downloaded the wrapper
175176

176177
const wrapperName = `haskell-language-server-wrapper-${release.tag_name}-${process.platform}${exeExt}`;
177-
const downloadedWrapper = path.join(context.globalStoragePath, wrapperName);
178+
const downloadedWrapper = path.join(storagePath, wrapperName);
178179

179180
if (executableExists(downloadedWrapper)) {
180181
return callWrapper(downloadedWrapper);
@@ -204,7 +205,7 @@ async function getProjectGhcVersion(
204205
return callWrapper(downloadedWrapper);
205206
}
206207

207-
async function getLatestReleaseMetadata(context: ExtensionContext): Promise<IRelease | null> {
208+
async function getLatestReleaseMetadata(context: ExtensionContext, storagePath: string): Promise<IRelease | null> {
208209
const releasesUrl = workspace.getConfiguration('haskell').releasesURL
209210
? url.parse(workspace.getConfiguration('haskell').releasesURL)
210211
: undefined;
@@ -218,7 +219,7 @@ async function getLatestReleaseMetadata(context: ExtensionContext): Promise<IRel
218219
path: '/repos/haskell/haskell-language-server/releases',
219220
};
220221

221-
const offlineCache = path.join(context.globalStoragePath, 'latestApprovedRelease.cache.json');
222+
const offlineCache = path.join(storagePath, 'latestApprovedRelease.cache.json');
222223

223224
async function readCachedReleaseData(): Promise<IRelease | null> {
224225
try {
@@ -293,8 +294,18 @@ export async function downloadHaskellLanguageServer(
293294
): Promise<string | null> {
294295
// Make sure to create this before getProjectGhcVersion
295296
logger.info('Downloading haskell-language-server');
296-
if (!fs.existsSync(context.globalStoragePath)) {
297-
fs.mkdirSync(context.globalStoragePath);
297+
298+
let storagePath: string | undefined = await workspace.getConfiguration('haskell').get('releasesDownloadStoragePath');
299+
300+
if (!storagePath) {
301+
storagePath = context.globalStorageUri.fsPath;
302+
} else {
303+
storagePath = resolvePathPlaceHolders(storagePath);
304+
}
305+
logger.info(`Using ${storagePath} to store downloaded binaries`);
306+
307+
if (!fs.existsSync(storagePath)) {
308+
fs.mkdirSync(storagePath);
298309
}
299310

300311
const githubOS = getGithubOS();
@@ -305,7 +316,7 @@ export async function downloadHaskellLanguageServer(
305316
}
306317

307318
logger.info('Fetching the latest release from GitHub or from cache');
308-
const release = await getLatestReleaseMetadata(context);
319+
const release = await getLatestReleaseMetadata(context, storagePath);
309320
if (!release) {
310321
let message = "Couldn't find any pre-built haskell-language-server binaries";
311322
const updateBehaviour = workspace.getConfiguration('haskell').get('updateBehavior') as UpdateBehaviour;
@@ -320,7 +331,7 @@ export async function downloadHaskellLanguageServer(
320331
const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath);
321332
let ghcVersion: string;
322333
try {
323-
ghcVersion = await getProjectGhcVersion(context, logger, dir, release);
334+
ghcVersion = await getProjectGhcVersion(context, logger, dir, release, storagePath);
324335
} catch (error) {
325336
if (error instanceof MissingToolError) {
326337
const link = error.installLink();
@@ -354,7 +365,7 @@ export async function downloadHaskellLanguageServer(
354365
}
355366

356367
const serverName = `haskell-language-server-${release.tag_name}-${process.platform}-${ghcVersion}${exeExt}`;
357-
const binaryDest = path.join(context.globalStoragePath, serverName);
368+
const binaryDest = path.join(storagePath, serverName);
358369

359370
const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`;
360371
logger.info(title);

src/utils.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import * as child_process from 'child_process';
44
import * as fs from 'fs';
55
import * as http from 'http';
66
import * as https from 'https';
7+
import * as os from 'os';
78
import { extname } from 'path';
89
import * as url from 'url';
910
import { promisify } from 'util';
10-
import { OutputChannel, ProgressLocation, window } from 'vscode';
11+
import { OutputChannel, ProgressLocation, window, WorkspaceFolder } from 'vscode';
1112
import { Logger } from 'vscode-languageclient';
1213
import * as yazul from 'yauzl';
1314
import { createGunzip } from 'zlib';
@@ -261,3 +262,11 @@ export function executableExists(exe: string): boolean {
261262
const out = child_process.spawnSync(cmd, [exe]);
262263
return out.status === 0 || (isWindows && fs.existsSync(exe));
263264
}
265+
266+
export function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder) {
267+
path = path.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
268+
if (folder) {
269+
path = path.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
270+
}
271+
return path;
272+
}

test/runTest.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as cp from 'child_process';
2+
import * as fs from 'fs';
23
import * as path from 'path';
34

45
import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron';
@@ -26,14 +27,18 @@ async function main() {
2627
// Passed to --extensionTestsPath
2728
const extensionTestsPath = path.resolve(__dirname, './suite/index');
2829

30+
const testWorkspace = path.resolve(__dirname, '../../test-workspace');
31+
32+
if (!fs.existsSync(testWorkspace)) {
33+
fs.mkdirSync(testWorkspace);
34+
}
35+
2936
// Download VS Code, unzip it and run the integration test
3037
await runTests({
3138
vscodeExecutablePath,
3239
extensionDevelopmentPath,
3340
extensionTestsPath,
34-
launchArgs: [
35-
// '--disable-extensions'
36-
],
41+
launchArgs: [testWorkspace],
3742
});
3843
} catch (err) {
3944
console.error(err);

test/suite/extension.test.ts

+95-13
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,108 @@
1+
// tslint:disable: no-console
12
import * as assert from 'assert';
2-
3-
// You can import and use all API from the 'vscode' module
4-
// as well as import your extension to test it
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
import { TextEncoder } from 'util';
56
import * as vscode from 'vscode';
6-
// import * as haskell from '../../extension';
7+
import { CommandNames } from '../../src/commands/constants';
8+
9+
function getExtension() {
10+
return vscode.extensions.getExtension('haskell.haskell');
11+
}
12+
13+
async function delay(ms: number) {
14+
return new Promise((resolve) => setTimeout(() => resolve(false), ms));
15+
}
16+
17+
async function withTimeout(seconds: number, f: Promise<any>) {
18+
return Promise.race([f, delay(seconds * 1000)]);
19+
}
20+
21+
function getHaskellConfig() {
22+
return vscode.workspace.getConfiguration('haskell');
23+
}
24+
25+
function getWorkspaceRoot() {
26+
return vscode.workspace.workspaceFolders![0];
27+
}
28+
29+
function getWorkspaceFile(name: string) {
30+
const wsroot = getWorkspaceRoot().uri;
31+
return wsroot.with({ path: path.posix.join(wsroot.path, name) });
32+
}
733

8-
function getExtension(extId: string) {
9-
return vscode.extensions.getExtension(extId);
34+
async function deleteWorkspaceFiles() {
35+
const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot().uri);
36+
console.log(`Deleting test ws contents: ${dirContents}`);
37+
dirContents.forEach(async ([name, type]) => {
38+
const uri: vscode.Uri = getWorkspaceFile(name);
39+
console.log(`Deleting ${uri}`);
40+
await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true });
41+
});
1042
}
1143

1244
suite('Extension Test Suite', () => {
45+
const disposables: vscode.Disposable[] = [];
46+
47+
async function existsWorkspaceFile(pattern: string, pred?: (uri: vscode.Uri) => boolean) {
48+
const relPath: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern);
49+
const watcher = vscode.workspace.createFileSystemWatcher(relPath);
50+
disposables.push(watcher);
51+
return new Promise<vscode.Uri>((resolve) => {
52+
watcher.onDidCreate((uri) => {
53+
console.log(`Created: ${uri}`);
54+
if (!pred || pred(uri)) {
55+
resolve(uri);
56+
}
57+
});
58+
});
59+
}
60+
1361
vscode.window.showInformationMessage('Start all tests.');
1462

63+
suiteSetup(async () => {
64+
await deleteWorkspaceFiles();
65+
await getHaskellConfig().update('logFile', 'hls.log');
66+
await getHaskellConfig().update('trace.server', 'messages');
67+
await getHaskellConfig().update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath));
68+
const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"');
69+
await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents);
70+
});
71+
1572
test('Extension should be present', () => {
16-
assert.ok(getExtension('haskell.haskell'));
73+
assert.ok(getExtension());
1774
});
1875

19-
test('should activate', () => {
20-
return getExtension('haskell.haskell')
21-
?.activate()
22-
.then(() => {
23-
assert.ok(true);
24-
});
76+
test('Extension should activate', async () => {
77+
await getExtension()?.activate();
78+
assert.ok(true);
79+
});
80+
81+
test('HLS executables should be downloaded', async () => {
82+
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
83+
const exeExt = os.platform.toString() === 'win32' ? '.exe' : '';
84+
console.log('Testing wrapper');
85+
const pred = (uri: vscode.Uri) => !['download', 'gz', 'zip'].includes(path.extname(uri.fsPath));
86+
assert.ok(
87+
await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}`, pred)),
88+
'The wrapper executable was not downloaded in 30 seconds'
89+
);
90+
console.log('Testing server');
91+
assert.ok(
92+
await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}`, pred)),
93+
'The server executable was not downloaded in 60 seconds'
94+
);
95+
});
96+
97+
test('Server log should be created', async () => {
98+
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
99+
assert.ok(await withTimeout(30, existsWorkspaceFile('hls.log')), 'Server log not created in 30 seconds');
100+
});
101+
102+
suiteTeardown(async () => {
103+
disposables.forEach((d) => d.dispose());
104+
await vscode.commands.executeCommand(CommandNames.StopServerCommandName);
105+
delay(5); // to give time to shutdown server
106+
await deleteWorkspaceFiles();
25107
});
26108
});

test/suite/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export async function run(): Promise<void> {
66
// Create the mocha test
77
const mocha = new Mocha({
88
ui: 'tdd',
9+
timeout: 90000,
910
});
1011
mocha.useColors(true);
1112

0 commit comments

Comments
 (0)