From 01b2ed531139adb697e53a31707fc239360c0352 Mon Sep 17 00:00:00 2001 From: jneira Date: Tue, 5 Oct 2021 23:49:49 +0200 Subject: [PATCH 01/14] workspace setup --- test/runTest.ts | 14 +++++++++++--- test/suite/extension.test.ts | 6 +++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/test/runTest.ts b/test/runTest.ts index c1c1b350..4114dee3 100644 --- a/test/runTest.ts +++ b/test/runTest.ts @@ -1,7 +1,9 @@ import * as cp from 'child_process'; +import * as fs from 'fs'; import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; +import { workspace } from 'vscode'; function installExtension(vscodeExePath: string, extId: string) { const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExePath); @@ -26,14 +28,20 @@ async function main() { // Passed to --extensionTestsPath const extensionTestsPath = path.resolve(__dirname, './suite/index'); + const testWorkspace = path.resolve(__dirname, '../../test-workspace'); + + if (!fs.existsSync(testWorkspace)) { + fs.mkdirSync(testWorkspace); + } + + await workspace.getConfiguration('haskell').update('haskell.logFile', path.join(testWorkspace, 'hls.log')); + // Download VS Code, unzip it and run the integration test await runTests({ vscodeExecutablePath, extensionDevelopmentPath, extensionTestsPath, - launchArgs: [ - // '--disable-extensions' - ], + launchArgs: [testWorkspace], }); } catch (err) { console.error(err); diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index ca62e47d..7878b2a0 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -16,11 +16,15 @@ suite('Extension Test Suite', () => { assert.ok(getExtension('haskell.haskell')); }); - test('should activate', () => { + test('Extension should activate', () => { return getExtension('haskell.haskell') ?.activate() .then(() => { assert.ok(true); }); }); + + test('Server should be activated', () => { + vscode.workspace.openTextDocument('Main.hs').then((textDoc) => {}); + }); }); From 61bfaa21cd776cfcecc4e0b75f59e39c678acb39 Mon Sep 17 00:00:00 2001 From: jneira Date: Wed, 6 Oct 2021 23:34:51 +0200 Subject: [PATCH 02/14] Move update config to tests --- test/runTest.ts | 3 --- test/suite/extension.test.ts | 9 +++++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/runTest.ts b/test/runTest.ts index 4114dee3..111b4a1b 100644 --- a/test/runTest.ts +++ b/test/runTest.ts @@ -3,7 +3,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { downloadAndUnzipVSCode, resolveCliPathFromVSCodeExecutablePath, runTests } from '@vscode/test-electron'; -import { workspace } from 'vscode'; function installExtension(vscodeExePath: string, extId: string) { const cliPath = resolveCliPathFromVSCodeExecutablePath(vscodeExePath); @@ -34,8 +33,6 @@ async function main() { fs.mkdirSync(testWorkspace); } - await workspace.getConfiguration('haskell').update('haskell.logFile', path.join(testWorkspace, 'hls.log')); - // Download VS Code, unzip it and run the integration test await runTests({ vscodeExecutablePath, diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 7878b2a0..84137729 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -1,5 +1,4 @@ import * as assert from 'assert'; - // You can import and use all API from the 'vscode' module // as well as import your extension to test it import * as vscode from 'vscode'; @@ -12,6 +11,10 @@ function getExtension(extId: string) { suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); + before(() => { + vscode.workspace.getConfiguration('haskell').update('haskell.logFile', 'hls.log'); + }); + test('Extension should be present', () => { assert.ok(getExtension('haskell.haskell')); }); @@ -25,6 +28,8 @@ suite('Extension Test Suite', () => { }); test('Server should be activated', () => { - vscode.workspace.openTextDocument('Main.hs').then((textDoc) => {}); + vscode.workspace.openTextDocument('Main.hs').then((textDoc) => { + assert.strictEqual(textDoc.languageId, 'haskell'); + }); }); }); From f0eca3fa02f97d55c86eff303a8320f8d5c9780f Mon Sep 17 00:00:00 2001 From: jneira Date: Thu, 7 Oct 2021 14:51:40 +0200 Subject: [PATCH 03/14] Initial setup --- package.json | 8 +++- src/extension.ts | 10 ++--- src/hlsBinaries.ts | 31 +++++++++----- src/utils.ts | 11 ++++- test/suite/extension.test.ts | 79 ++++++++++++++++++++++++++++-------- test/suite/index.ts | 1 + 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 1bd6fe26..d3b34122 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "haskell", "displayName": "Haskell", "description": "Haskell language support powered by the Haskell Language Server", - "version": "1.7.1", + "version": "1.7.2", "license": "MIT", "publisher": "haskell", "engines": { @@ -126,6 +126,12 @@ "default": "", "description": "An optional URL to override where to check for haskell-language-server releases" }, + "haskell.releasesDownloadStoragePath": { + "scope": "resource", + "type": "string", + "default": "", + "markdownDescription": "An optional path where downloaded binaries will be stored. Check the default value [here](https://github.com/haskell/vscode-haskell#downloaded-binaries)" + }, "haskell.serverExecutablePath": { "scope": "resource", "type": "string", diff --git a/src/extension.ts b/src/extension.ts index 3c5c63d5..592e858f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,5 +1,4 @@ 'use strict'; -import * as os from 'os'; import * as path from 'path'; import { commands, @@ -24,7 +23,7 @@ import { CommandNames } from './commands/constants'; import { ImportIdentifier } from './commands/importIdentifier'; import { DocsBrowser } from './docsBrowser'; import { downloadHaskellLanguageServer } from './hlsBinaries'; -import { executableExists, ExtensionLogger } from './utils'; +import { executableExists, ExtensionLogger, resolvePathPlaceHolders } from './utils'; // The current map of documents & folders to language servers. // 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 return null; } logger.info(`Trying to find the server executable in: ${exePath}`); - // Substitute path variables with their corresponding locations. - exePath = exePath.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir); - if (folder) { - exePath = exePath.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path); - } + exePath = resolvePathPlaceHolders(exePath, folder); logger.info(`Location after path variables subsitution: ${exePath}`); + if (!executableExists(exePath)) { throw new Error(`serverExecutablePath is set to ${exePath} but it doesn't exist and it is not on the PATH`); } diff --git a/src/hlsBinaries.ts b/src/hlsBinaries.ts index fbab95d5..7166cb9b 100644 --- a/src/hlsBinaries.ts +++ b/src/hlsBinaries.ts @@ -7,7 +7,7 @@ import * as url from 'url'; import { promisify } from 'util'; import { env, ExtensionContext, ProgressLocation, Uri, window, workspace, WorkspaceFolder } from 'vscode'; import { Logger } from 'vscode-languageclient'; -import { downloadFile, executableExists, httpsGetSilently } from './utils'; +import { downloadFile, executableExists, httpsGetSilently, resolvePathPlaceHolders } from './utils'; import * as validate from './validation'; /** GitHub API release */ @@ -102,7 +102,8 @@ async function getProjectGhcVersion( context: ExtensionContext, logger: Logger, dir: string, - release: IRelease + release: IRelease, + storagePath: string ): Promise { const title: string = 'Working out the project GHC version. This might take a while...'; logger.info(title); @@ -174,7 +175,7 @@ async function getProjectGhcVersion( // Otherwise search to see if we previously downloaded the wrapper const wrapperName = `haskell-language-server-wrapper-${release.tag_name}-${process.platform}${exeExt}`; - const downloadedWrapper = path.join(context.globalStoragePath, wrapperName); + const downloadedWrapper = path.join(storagePath, wrapperName); if (executableExists(downloadedWrapper)) { return callWrapper(downloadedWrapper); @@ -204,7 +205,7 @@ async function getProjectGhcVersion( return callWrapper(downloadedWrapper); } -async function getLatestReleaseMetadata(context: ExtensionContext): Promise { +async function getLatestReleaseMetadata(context: ExtensionContext, storagePath: string): Promise { const releasesUrl = workspace.getConfiguration('haskell').releasesURL ? url.parse(workspace.getConfiguration('haskell').releasesURL) : undefined; @@ -218,7 +219,7 @@ async function getLatestReleaseMetadata(context: ExtensionContext): Promise { try { @@ -293,8 +294,18 @@ export async function downloadHaskellLanguageServer( ): Promise { // Make sure to create this before getProjectGhcVersion logger.info('Downloading haskell-language-server'); - if (!fs.existsSync(context.globalStoragePath)) { - fs.mkdirSync(context.globalStoragePath); + + let storagePath: string | undefined = await workspace.getConfiguration('haskell').get('releasesDownloadStoragePath'); + + if (!storagePath) { + storagePath = context.globalStorageUri.fsPath; + } else { + storagePath = resolvePathPlaceHolders(storagePath); + } + logger.info(`Using ${storagePath} to store downloaded binaries`); + + if (!fs.existsSync(storagePath)) { + fs.mkdirSync(storagePath); } const githubOS = getGithubOS(); @@ -305,7 +316,7 @@ export async function downloadHaskellLanguageServer( } logger.info('Fetching the latest release from GitHub or from cache'); - const release = await getLatestReleaseMetadata(context); + const release = await getLatestReleaseMetadata(context, storagePath); if (!release) { let message = "Couldn't find any pre-built haskell-language-server binaries"; const updateBehaviour = workspace.getConfiguration('haskell').get('updateBehavior') as UpdateBehaviour; @@ -320,7 +331,7 @@ export async function downloadHaskellLanguageServer( const dir: string = folder?.uri?.fsPath ?? path.dirname(resource.fsPath); let ghcVersion: string; try { - ghcVersion = await getProjectGhcVersion(context, logger, dir, release); + ghcVersion = await getProjectGhcVersion(context, logger, dir, release, storagePath); } catch (error) { if (error instanceof MissingToolError) { const link = error.installLink(); @@ -354,7 +365,7 @@ export async function downloadHaskellLanguageServer( } const serverName = `haskell-language-server-${release.tag_name}-${process.platform}-${ghcVersion}${exeExt}`; - const binaryDest = path.join(context.globalStoragePath, serverName); + const binaryDest = path.join(storagePath, serverName); const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`; logger.info(title); diff --git a/src/utils.ts b/src/utils.ts index 5c8a9b9f..c79025a5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,10 +4,11 @@ import * as child_process from 'child_process'; import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; +import * as os from 'os'; import { extname } from 'path'; import * as url from 'url'; import { promisify } from 'util'; -import { OutputChannel, ProgressLocation, window } from 'vscode'; +import { OutputChannel, ProgressLocation, window, WorkspaceFolder } from 'vscode'; import { Logger } from 'vscode-languageclient'; import * as yazul from 'yauzl'; import { createGunzip } from 'zlib'; @@ -261,3 +262,11 @@ export function executableExists(exe: string): boolean { const out = child_process.spawnSync(cmd, [exe]); return out.status === 0 || (isWindows && fs.existsSync(exe)); } + +export function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder) { + path = path.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir); + if (folder) { + path = path.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path); + } + return path; +} diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 84137729..79d7aa62 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -1,35 +1,80 @@ import * as assert from 'assert'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it +import * as os from 'os'; +import * as path from 'path'; +import { TextEncoder } from 'util'; import * as vscode from 'vscode'; -// import * as haskell from '../../extension'; -function getExtension(extId: string) { - return vscode.extensions.getExtension(extId); +function getExtension() { + return vscode.extensions.getExtension('haskell.haskell'); +} + +async function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function getHaskellConfig() { + return vscode.workspace.getConfiguration('haskell'); +} + +function getWorkspaceRoot() { + return vscode.workspace.workspaceFolders![0]!.uri; +} + +function getWorkspaceFile(name: string) { + const wsroot = getWorkspaceRoot(); + return wsroot.with({ path: path.posix.join(wsroot.path, name) }); +} + +async function existsWorkspaceFile(fileRelativePath: string) { + const files = await vscode.workspace.findFiles(`**/${fileRelativePath}`); + return files.length === 1; } suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); - before(() => { - vscode.workspace.getConfiguration('haskell').update('haskell.logFile', 'hls.log'); + suiteSetup(async () => { + await getHaskellConfig().update('logFile', 'hls.log'); + await getHaskellConfig().update('trace.server', 'messages'); + await getHaskellConfig().update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath)); + const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"'); + await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents); }); test('Extension should be present', () => { - assert.ok(getExtension('haskell.haskell')); + assert.ok(getExtension()); + }); + + test('Extension should activate', async () => { + await getExtension()?.activate(); + assert.ok(true); + }); + + test('Server executables should be downloaded', async () => { + await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); + const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; + await delay(30 * 1000); + assert.ok( + await existsWorkspaceFile(`/bin/haskell-language-server-wrapper${exeExt}`), + 'The wrapper executable was not downloaded in 15 seconds' + ); + await delay(30 * 1000); + assert.ok( + await existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`), + 'The server executable was not downloaded in 15 seconds' + ); }); - test('Extension should activate', () => { - return getExtension('haskell.haskell') - ?.activate() - .then(() => { - assert.ok(true); - }); + test('Server log should be created', async () => { + await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); + await delay(5 * 1000); + assert.ok(await existsWorkspaceFile('hls.log'), 'Server log not created in 5 seconds'); }); - test('Server should be activated', () => { - vscode.workspace.openTextDocument('Main.hs').then((textDoc) => { - assert.strictEqual(textDoc.languageId, 'haskell'); + suiteTeardown(async () => { + const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot()); + dirContents.forEach(async ([name, type]) => { + await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true }); }); }); }); diff --git a/test/suite/index.ts b/test/suite/index.ts index 6df7da45..a7832450 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -6,6 +6,7 @@ export async function run(): Promise { // Create the mocha test const mocha = new Mocha({ ui: 'tdd', + timeout: 60000, }); mocha.useColors(true); From f883a2acf3c5aadeb2c8750e203a4bebbc07ce29 Mon Sep 17 00:00:00 2001 From: jneira Date: Thu, 7 Oct 2021 21:22:52 +0200 Subject: [PATCH 04/14] Use withTimeout --- test/suite/extension.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 79d7aa62..7b3af027 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -12,6 +12,10 @@ async function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +async function withTimeout(seconds: number, f: Promise) { + Promise.race([f, delay(seconds * 1000)]); +} + function getHaskellConfig() { return vscode.workspace.getConfiguration('haskell'); } @@ -53,22 +57,19 @@ suite('Extension Test Suite', () => { test('Server executables should be downloaded', async () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; - await delay(30 * 1000); assert.ok( - await existsWorkspaceFile(`/bin/haskell-language-server-wrapper${exeExt}`), + await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper${exeExt}`)), 'The wrapper executable was not downloaded in 15 seconds' ); - await delay(30 * 1000); assert.ok( - await existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`), + await withTimeout(30, existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`)), 'The server executable was not downloaded in 15 seconds' ); }); test('Server log should be created', async () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); - await delay(5 * 1000); - assert.ok(await existsWorkspaceFile('hls.log'), 'Server log not created in 5 seconds'); + assert.ok(await withTimeout(5, existsWorkspaceFile('hls.log')), 'Server log not created in 5 seconds'); }); suiteTeardown(async () => { From 91767cecec41dc5a66c8a7015d273ddab8a2eaaf Mon Sep 17 00:00:00 2001 From: jneira Date: Thu, 7 Oct 2021 23:55:25 +0200 Subject: [PATCH 05/14] Correct race --- test/suite/extension.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 7b3af027..2b9495bd 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -3,17 +3,18 @@ import * as os from 'os'; import * as path from 'path'; import { TextEncoder } from 'util'; import * as vscode from 'vscode'; +import { CommandNames } from '../../src/commands/constants'; function getExtension() { return vscode.extensions.getExtension('haskell.haskell'); } async function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); + return new Promise((_, reject) => setTimeout(reject, ms)); } async function withTimeout(seconds: number, f: Promise) { - Promise.race([f, delay(seconds * 1000)]); + return Promise.race([f, delay(seconds * 1000)]); } function getHaskellConfig() { @@ -73,6 +74,7 @@ suite('Extension Test Suite', () => { }); suiteTeardown(async () => { + await vscode.commands.executeCommand(CommandNames.StopServerCommandName); const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot()); dirContents.forEach(async ([name, type]) => { await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true }); From 9dc90984024292cb58934e1caa0e80dc8b968b84 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 08:32:17 +0200 Subject: [PATCH 06/14] Use fileSystemWatcher to make race work --- test/suite/extension.test.ts | 18 ++++++++++++++---- test/suite/index.ts | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 2b9495bd..b0e6698c 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -10,7 +10,7 @@ function getExtension() { } async function delay(ms: number) { - return new Promise((_, reject) => setTimeout(reject, ms)); + return new Promise((_, reject) => setTimeout(() => reject(`Timeout of ${ms} ms reached.`), ms)); } async function withTimeout(seconds: number, f: Promise) { @@ -31,8 +31,14 @@ function getWorkspaceFile(name: string) { } async function existsWorkspaceFile(fileRelativePath: string) { - const files = await vscode.workspace.findFiles(`**/${fileRelativePath}`); - return files.length === 1; + return new Promise((resolve) => { + // tslint:disable: no-console + console.log(`Creating file system watcher for ${fileRelativePath}`); + vscode.workspace.createFileSystemWatcher(`**/${fileRelativePath}`).onDidCreate((uri) => { + console.log(`Created: ${uri}`); + resolve(uri); + }); + }); } suite('Extension Test Suite', () => { @@ -63,7 +69,7 @@ suite('Extension Test Suite', () => { 'The wrapper executable was not downloaded in 15 seconds' ); assert.ok( - await withTimeout(30, existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`)), + await withTimeout(60, existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`)), 'The server executable was not downloaded in 15 seconds' ); }); @@ -76,7 +82,11 @@ suite('Extension Test Suite', () => { suiteTeardown(async () => { await vscode.commands.executeCommand(CommandNames.StopServerCommandName); const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot()); + // tslint:disable: no-console + console.log(`Deleting test ws contents: ${dirContents}`); dirContents.forEach(async ([name, type]) => { + const uri: vscode.Uri = getWorkspaceFile(name); + console.log(`Deleting ${uri}`); await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true }); }); }); diff --git a/test/suite/index.ts b/test/suite/index.ts index a7832450..ec26a951 100644 --- a/test/suite/index.ts +++ b/test/suite/index.ts @@ -6,7 +6,7 @@ export async function run(): Promise { // Create the mocha test const mocha = new Mocha({ ui: 'tdd', - timeout: 60000, + timeout: 90000, }); mocha.useColors(true); From 901d4a57d139afd4b653301a7619e40509077081 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 08:50:52 +0200 Subject: [PATCH 07/14] dispose fileSystemWatchers --- test/suite/extension.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index b0e6698c..a1da0786 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -3,6 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import { TextEncoder } from 'util'; import * as vscode from 'vscode'; +import { Disposable } from 'vscode-languageserver-protocol'; import { CommandNames } from '../../src/commands/constants'; function getExtension() { @@ -30,14 +31,17 @@ function getWorkspaceFile(name: string) { return wsroot.with({ path: path.posix.join(wsroot.path, name) }); } +const disposables: Disposable[] = []; + async function existsWorkspaceFile(fileRelativePath: string) { return new Promise((resolve) => { // tslint:disable: no-console console.log(`Creating file system watcher for ${fileRelativePath}`); - vscode.workspace.createFileSystemWatcher(`**/${fileRelativePath}`).onDidCreate((uri) => { + const watcher = vscode.workspace.createFileSystemWatcher(`**/${fileRelativePath}`).onDidCreate((uri) => { console.log(`Created: ${uri}`); resolve(uri); }); + disposables.push(watcher); }); } @@ -66,11 +70,11 @@ suite('Extension Test Suite', () => { const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; assert.ok( await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper${exeExt}`)), - 'The wrapper executable was not downloaded in 15 seconds' + 'The wrapper executable was not downloaded in 30 seconds' ); assert.ok( - await withTimeout(60, existsWorkspaceFile(`/bin/haskell-language-server${exeExt}`)), - 'The server executable was not downloaded in 15 seconds' + await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server${exeExt}`)), + 'The server executable was not downloaded in 30 seconds' ); }); @@ -80,6 +84,7 @@ suite('Extension Test Suite', () => { }); suiteTeardown(async () => { + disposables.forEach((d) => d.dispose()); await vscode.commands.executeCommand(CommandNames.StopServerCommandName); const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot()); // tslint:disable: no-console From b34e8b580ad5dd5a0b629d5e828ec5df48c0b8d5 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 08:57:27 +0200 Subject: [PATCH 08/14] Ignore test ws --- .gitignore | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ee361aa9..4bb73a38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ -out -node_modules -.vscode-test -.DS_Store -dist +out +node_modules +.vscode-test +test-workspace +.DS_Store +dist *.vsix \ No newline at end of file From 518707c93fbaa443ba67adc86242a93e58e69de9 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 08:58:14 +0200 Subject: [PATCH 09/14] Tweak glob --- test/suite/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index a1da0786..9a554c24 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -37,7 +37,7 @@ async function existsWorkspaceFile(fileRelativePath: string) { return new Promise((resolve) => { // tslint:disable: no-console console.log(`Creating file system watcher for ${fileRelativePath}`); - const watcher = vscode.workspace.createFileSystemWatcher(`**/${fileRelativePath}`).onDidCreate((uri) => { + const watcher = vscode.workspace.createFileSystemWatcher(`**${fileRelativePath}`).onDidCreate((uri) => { console.log(`Created: ${uri}`); resolve(uri); }); From 56e0aeaeff4ec17f6dc44fca040c36c226830c29 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 09:09:15 +0200 Subject: [PATCH 10/14] Use RelativePattern --- test/suite/extension.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 9a554c24..27b17a92 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -1,3 +1,4 @@ +// tslint:disable: no-console import * as assert from 'assert'; import * as os from 'os'; import * as path from 'path'; @@ -23,21 +24,21 @@ function getHaskellConfig() { } function getWorkspaceRoot() { - return vscode.workspace.workspaceFolders![0]!.uri; + return vscode.workspace.workspaceFolders![0]; } function getWorkspaceFile(name: string) { - const wsroot = getWorkspaceRoot(); + const wsroot = getWorkspaceRoot().uri; return wsroot.with({ path: path.posix.join(wsroot.path, name) }); } const disposables: Disposable[] = []; -async function existsWorkspaceFile(fileRelativePath: string) { +async function existsWorkspaceFile(pattern: string) { return new Promise((resolve) => { - // tslint:disable: no-console - console.log(`Creating file system watcher for ${fileRelativePath}`); - const watcher = vscode.workspace.createFileSystemWatcher(`**${fileRelativePath}`).onDidCreate((uri) => { + const pat: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern); + console.log(`Creating file system watcher for ${pat}`); + const watcher = vscode.workspace.createFileSystemWatcher(pat).onDidCreate((uri) => { console.log(`Created: ${uri}`); resolve(uri); }); @@ -86,8 +87,7 @@ suite('Extension Test Suite', () => { suiteTeardown(async () => { disposables.forEach((d) => d.dispose()); await vscode.commands.executeCommand(CommandNames.StopServerCommandName); - const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot()); - // tslint:disable: no-console + const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot().uri); console.log(`Deleting test ws contents: ${dirContents}`); dirContents.forEach(async ([name, type]) => { const uri: vscode.Uri = getWorkspaceFile(name); From e08a261106c4d6fcc16d457c200225394bd58457 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 09:46:41 +0200 Subject: [PATCH 11/14] Move file watchers to test suite scope --- test/suite/extension.test.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 27b17a92..74f2fc63 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -4,7 +4,6 @@ import * as os from 'os'; import * as path from 'path'; import { TextEncoder } from 'util'; import * as vscode from 'vscode'; -import { Disposable } from 'vscode-languageserver-protocol'; import { CommandNames } from '../../src/commands/constants'; function getExtension() { @@ -32,21 +31,21 @@ function getWorkspaceFile(name: string) { return wsroot.with({ path: path.posix.join(wsroot.path, name) }); } -const disposables: Disposable[] = []; +suite('Extension Test Suite', () => { + const disposables: vscode.Disposable[] = []; -async function existsWorkspaceFile(pattern: string) { - return new Promise((resolve) => { - const pat: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern); - console.log(`Creating file system watcher for ${pat}`); - const watcher = vscode.workspace.createFileSystemWatcher(pat).onDidCreate((uri) => { - console.log(`Created: ${uri}`); - resolve(uri); - }); + async function existsWorkspaceFile(pattern: string) { + const relPath: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern); + const watcher = vscode.workspace.createFileSystemWatcher(relPath); disposables.push(watcher); - }); -} + return new Promise((resolve) => { + watcher.onDidCreate((uri) => { + console.log(`Created: ${uri}`); + resolve(uri); + }); + }); + } -suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); suiteSetup(async () => { @@ -66,13 +65,15 @@ suite('Extension Test Suite', () => { assert.ok(true); }); - test('Server executables should be downloaded', async () => { + test('HLS executables should be downloaded', async () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; + console.log('Testing wrapper'); assert.ok( await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper${exeExt}`)), 'The wrapper executable was not downloaded in 30 seconds' ); + console.log('Testing server'); assert.ok( await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server${exeExt}`)), 'The server executable was not downloaded in 30 seconds' From 6f861066f94f18c96405ea9bb92cdde6fc38a3c2 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 10:17:51 +0200 Subject: [PATCH 12/14] Got globs right :facepalm: --- test/suite/extension.test.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 74f2fc63..ae0594ca 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -11,7 +11,7 @@ function getExtension() { } async function delay(ms: number) { - return new Promise((_, reject) => setTimeout(() => reject(`Timeout of ${ms} ms reached.`), ms)); + return new Promise((resolve) => setTimeout(() => resolve(false), ms)); } async function withTimeout(seconds: number, f: Promise) { @@ -31,6 +31,16 @@ function getWorkspaceFile(name: string) { return wsroot.with({ path: path.posix.join(wsroot.path, name) }); } +async function deleteWorkspaceFiles() { + const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot().uri); + console.log(`Deleting test ws contents: ${dirContents}`); + dirContents.forEach(async ([name, type]) => { + const uri: vscode.Uri = getWorkspaceFile(name); + console.log(`Deleting ${uri}`); + await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true }); + }); +} + suite('Extension Test Suite', () => { const disposables: vscode.Disposable[] = []; @@ -49,6 +59,7 @@ suite('Extension Test Suite', () => { vscode.window.showInformationMessage('Start all tests.'); suiteSetup(async () => { + await deleteWorkspaceFiles(); await getHaskellConfig().update('logFile', 'hls.log'); await getHaskellConfig().update('trace.server', 'messages'); await getHaskellConfig().update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath)); @@ -70,30 +81,25 @@ suite('Extension Test Suite', () => { const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; console.log('Testing wrapper'); assert.ok( - await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper${exeExt}`)), + await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}[!.]`)), 'The wrapper executable was not downloaded in 30 seconds' ); console.log('Testing server'); assert.ok( - await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server${exeExt}`)), + await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}[!.]`)), 'The server executable was not downloaded in 30 seconds' ); }); test('Server log should be created', async () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); - assert.ok(await withTimeout(5, existsWorkspaceFile('hls.log')), 'Server log not created in 5 seconds'); + assert.ok(await withTimeout(30, existsWorkspaceFile('hls.log')), 'Server log not created in 30 seconds'); }); suiteTeardown(async () => { disposables.forEach((d) => d.dispose()); await vscode.commands.executeCommand(CommandNames.StopServerCommandName); - const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot().uri); - console.log(`Deleting test ws contents: ${dirContents}`); - dirContents.forEach(async ([name, type]) => { - const uri: vscode.Uri = getWorkspaceFile(name); - console.log(`Deleting ${uri}`); - await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true }); - }); + delay(5); // to give time to shutdown server + await deleteWorkspaceFiles(); }); }); From 8d1dfadd7cb9552336684d2fc7a64e52655b2922 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 11:11:00 +0200 Subject: [PATCH 13/14] More fine grained file check --- test/suite/extension.test.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index ae0594ca..ac2e118c 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -44,14 +44,16 @@ async function deleteWorkspaceFiles() { suite('Extension Test Suite', () => { const disposables: vscode.Disposable[] = []; - async function existsWorkspaceFile(pattern: string) { + async function existsWorkspaceFile(pattern: string, pred?: (uri: vscode.Uri) => boolean) { const relPath: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern); const watcher = vscode.workspace.createFileSystemWatcher(relPath); disposables.push(watcher); return new Promise((resolve) => { watcher.onDidCreate((uri) => { console.log(`Created: ${uri}`); - resolve(uri); + if (!pred || pred(uri)) { + resolve(uri); + } }); }); } @@ -80,14 +82,15 @@ suite('Extension Test Suite', () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); const exeExt = os.platform.toString() === 'win32' ? '.exe' : ''; console.log('Testing wrapper'); + const pred = (uri: vscode.Uri) => !['download', 'gz', 'zip'].includes(path.extname(uri.fsPath)); assert.ok( - await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}[!.]`)), + await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}`, pred)), 'The wrapper executable was not downloaded in 30 seconds' ); console.log('Testing server'); assert.ok( - await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}[!.]`)), - 'The server executable was not downloaded in 30 seconds' + await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}`, pred)), + 'The server executable was not downloaded in 60 seconds' ); }); From 453c98a3de597916fc5e35f8ccb409a9cfc55e99 Mon Sep 17 00:00:00 2001 From: jneira Date: Fri, 8 Oct 2021 11:53:59 +0200 Subject: [PATCH 14/14] Use macos-11 in gha --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75975398..a2b4e811 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: build: strategy: matrix: - os: [macos-latest, ubuntu-latest, windows-latest] + os: [macos-11, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout