diff --git a/.eslintrc.json b/.eslintrc.json index 4cbf3ae4..6ecf9667 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,13 +3,23 @@ "browser": true, "es2021": true }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], "overrides": [], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["@typescript-eslint"], - "rules": {} -} + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "semi": [ + "error", + "always" + ] + } +} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 364f6cf0..fdaa68f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: fail-fast: false matrix: os: [macos-11, ubuntu-latest, windows-latest] - ghc: [9.0.2, 8.10.4] + ghc: [9.0.2, 8.10.4, 9.2.7, 9.4.5, 9.6.1] runs-on: ${{ matrix.os }} steps: - name: Checkout @@ -31,7 +31,9 @@ jobs: - name: Install cabal run: ghcup install cabal recommended - name: Install GHC - run: ghcup install ghc ${{matrix.ghc}} + run: | + ghcup install ghc ${{matrix.ghc}} + ghcup set ghc ${{matrix.ghc}} # Pre-fetch HLS binaries before the tests because otherwise # we run into timeouts. Downloading takes longer, since we download # per HLS version one HLS binary per GHC version. diff --git a/package.json b/package.json index b18b7eb8..98f07bbc 100644 --- a/package.json +++ b/package.json @@ -598,4 +598,4 @@ "which": "^3.0.0", "yauzl": "^2.10.0" } -} +} \ No newline at end of file diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 0dcadcd8..77bcdced 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -1,142 +1,144 @@ -// tslint:disable: no-console +// We have the following testing targets: +// 1. Test if the extension is present +// 2. Test if the extension can be activated +// 3. Test if the extension can create the extension log file +// 4. Test if the extension log contains server output (currently we use this to ensure the server is activated successfully) +// 5. Test if the server inherit environment variables defined in the settings (why?) + +import * as vscode from 'vscode'; import * as assert from 'assert'; +import path = require('path'); import * as fs from 'fs'; -import * as path from 'path'; -import { TextEncoder } from 'util'; -import * as vscode from 'vscode'; import { StopServerCommandName } from '../../src/commands/constants'; -function getExtension() { - return vscode.extensions.getExtension('haskell.haskell'); -} - -async function delay(seconds: number) { - return new Promise((resolve) => setTimeout(() => resolve(false), seconds * 1000)); -} - -async function withTimeout(seconds: number, f: Promise) { - return Promise.race([f, delay(seconds)]); -} - -const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -const retryOperation = (operation: () => Promise, delay: number, retries: number) => - new Promise((resolve, reject): Promise => { - return operation() - .then(resolve) - .catch((reason) => { - if (retries > 0) { - return wait(delay) - .then(retryOperation.bind(null, operation, delay, retries - 1)) - .then(resolve) - .catch(reject); - } - return reject(reason); - }); - }); +suite('Extension Test Suite', () => { + const extension = vscode.extensions.getExtension('haskell.haskell'); + const haskellConfig = vscode.workspace.getConfiguration('haskell'); + const filesCreated: Map> = new Map(); + const disposables: vscode.Disposable[] = []; -function getHaskellConfig() { - return vscode.workspace.getConfiguration('haskell'); -} - -function getWorkspaceRoot(): vscode.WorkspaceFolder { - return vscode.workspace.workspaceFolders![0]; -} - -function getWorkspaceFile(name: string): vscode.Uri { - const wsroot = getWorkspaceRoot().uri; - return wsroot.with({ path: path.posix.join(wsroot.path, name) }); -} - -function joinUri(root: vscode.Uri, ...pathSegments: string[]): vscode.Uri { - return root.with({ path: path.posix.join(root.path, ...pathSegments) }); -} - -async function deleteWorkspaceFiles(keepDirs: vscode.Uri[], pred?: (fileName: string) => boolean): Promise { - await deleteFiles(getWorkspaceRoot().uri, keepDirs, pred); -} - -function getExtensionLogContent(): string | undefined { - const extLog = getWorkspaceFile('hls.log').fsPath; - if (fs.existsSync(extLog)) { - const logContents = fs.readFileSync(extLog); - return logContents.toString(); - } else { - console.log(`${extLog} does not exist!`); - return undefined; - } -} - -async function deleteFiles(dir: vscode.Uri, keepDirs: vscode.Uri[], pred?: (fileType: string) => boolean) { - const dirContents = await vscode.workspace.fs.readDirectory(dir); - console.log(`Looking at ${dir} contents: ${dirContents}`); - if (keepDirs.findIndex((val) => val.path === dir.path) !== -1) { - console.log(`Keeping ${dir}`); - } else { - dirContents.forEach(async ([name, type]) => { - const uri: vscode.Uri = joinUri(dir, name); - if (type === vscode.FileType.File) { - if (!pred || pred(name)) { - console.log(`Deleting ${uri}`); - await vscode.workspace.fs.delete(joinUri(dir, name), { - recursive: false, - useTrash: false, - }); - } - } else if (type === vscode.FileType.Directory) { - const subDirectory = joinUri(dir, name); - console.log(`Recursing into ${subDirectory}`); - await deleteFiles(subDirectory, keepDirs, pred); - - // remove directory if it is empty now - const isEmptyNow = await vscode.workspace.fs - .readDirectory(subDirectory) - .then((contents) => Promise.resolve(contents.length === 0)); - if (isEmptyNow) { - console.log(`Deleting ${subDirectory}`); - await vscode.workspace.fs.delete(subDirectory, { - recursive: true, - useTrash: false, - }); - } - } - }); + function getWorkspaceRoot(): vscode.WorkspaceFolder { + const folders = vscode.workspace.workspaceFolders; + if (folders) { + return folders[0]; + } else { + throw "workspaceFolders is empty"; + } } -} -suite('Extension Test Suite', () => { - const disposables: vscode.Disposable[] = []; - const filesCreated: Map> = new Map(); + function getWorkspaceFile(name: string): vscode.Uri { + const wsroot = getWorkspaceRoot().uri; + return wsroot.with({ path: path.posix.join(wsroot.path, name) }); + } - async function existsWorkspaceFile(pattern: string, pred?: (uri: vscode.Uri) => boolean) { + 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}`); - if (!pred || pred(uri)) { - resolve(uri); - } + resolve(uri); }); }); } - vscode.window.showInformationMessage('Start all tests.'); + function getExtensionLogContent(): string | undefined { + const extLog = getWorkspaceFile('hls.log').fsPath; + if (fs.existsSync(extLog)) { + const logContents = fs.readFileSync(extLog); + return logContents.toString(); + } else { + console.log(`${extLog} does not exist!`); + return undefined; + } + } + + async function delay(seconds: number) { + return new Promise((resolve) => setTimeout(() => resolve(false), seconds * 1000)); + } + + async function withTimeout(seconds: number, f: Promise) { + return Promise.race([f, delay(seconds)]); + } + + const wait = (ms: number) => new Promise(r => setTimeout(r, ms)); + + const retryOperation = (operation: () => Promise, delay: number, retries: number) => + new Promise((resolve, reject): Promise => { + return operation() + .then(resolve) + .catch((reason) => { + if (retries > 0) { + console.log(`${Date.now()}`); + return wait(delay) + .then(retryOperation.bind(null, operation, delay, retries - 1)) + .then(resolve) + .catch(reject); + } + return reject(reason); + }); + }); + + function joinUri(root: vscode.Uri, ...pathSegments: string[]): vscode.Uri { + return root.with({ path: path.posix.join(root.path, ...pathSegments) }); + } + + async function deleteWorkspaceFiles(keepDirs: vscode.Uri[], pred?: (fileName: string) => boolean): Promise { + await deleteFiles(getWorkspaceRoot().uri, keepDirs, pred); + } + + async function deleteFiles(dir: vscode.Uri, keepDirs: vscode.Uri[], pred?: (fileType: string) => boolean) { + const dirContents = await vscode.workspace.fs.readDirectory(dir); + console.log(`Looking at ${dir} contents: ${dirContents}`); + if (keepDirs.findIndex((val) => val.path === dir.path) !== -1) { + console.log(`Keeping ${dir}`); + } else { + dirContents.forEach(async ([name, type]) => { + const uri: vscode.Uri = joinUri(dir, name); + if (type === vscode.FileType.File) { + if (!pred || pred(name)) { + console.log(`Deleting ${uri}`); + await vscode.workspace.fs.delete(joinUri(dir, name), { + recursive: false, + useTrash: false, + }); + } + } else if (type === vscode.FileType.Directory) { + const subDirectory = joinUri(dir, name); + console.log(`Recursing into ${subDirectory}`); + await deleteFiles(subDirectory, keepDirs, pred); + + // remove directory if it is empty now + const isEmptyNow = await vscode.workspace.fs + .readDirectory(subDirectory) + .then((contents) => Promise.resolve(contents.length === 0)); + if (isEmptyNow) { + console.log(`Deleting ${subDirectory}`); + await vscode.workspace.fs.delete(subDirectory, { + recursive: true, + useTrash: false, + }); + } + } + }); + } + } suiteSetup(async () => { await deleteWorkspaceFiles([ joinUri(getWorkspaceRoot().uri, '.vscode'), joinUri(getWorkspaceRoot().uri, 'bin', process.platform === 'win32' ? 'ghcup' : '.ghcup', 'cache'), ]); - await getHaskellConfig().update('promptBeforeDownloads', false, vscode.ConfigurationTarget.Global); - await getHaskellConfig().update('manageHLS', 'GHCup'); - await getHaskellConfig().update('logFile', 'hls.log'); - await getHaskellConfig().update('trace.server', 'messages'); - await getHaskellConfig().update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath)); - await getHaskellConfig().update('serverEnvironment', { + await haskellConfig.update('promptBeforeDownloads', false, vscode.ConfigurationTarget.Global); + await haskellConfig.update('manageHLS', 'GHCup'); + await haskellConfig.update('logFile', 'hls.log'); + await haskellConfig.update('trace.server', 'messages'); + await haskellConfig.update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath)); + await haskellConfig.update('serverEnvironment', { XDG_CACHE_HOME: path.normalize(getWorkspaceFile('cache-test').fsPath), }); + const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"'); await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents); @@ -144,57 +146,64 @@ suite('Extension Test Suite', () => { filesCreated.set('cache', existsWorkspaceFile('cache-test')); }); - test('Extension should be present', () => { - assert.ok(getExtension()); + test('1. Extension should be present', () => { + assert.ok(extension); }); - test('Extension should activate', async () => { - await getExtension()?.activate(); - assert.ok(true); + test('2. Extension can be activated', async () => { + await extension?.activate(); }); - test('Extension should create the extension log file', async () => { - await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); - assert.ok(await withTimeout(90, filesCreated.get('log')!), 'Extension log not created in 30 seconds'); + test('3. Extension should create the extension log file', async () => { + vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + assert.ok(await withTimeout(30, filesCreated.get('log')!), 'Extension log not created in 30 seconds'); }); - test('Extension log should have server output', async () => { - await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); - await delay(20); - const logContents = getExtensionLogContent(); - assert.ok(logContents, 'Extension log file does not exist'); + test('4. Extension log should have server output', async () => { + vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); assert.ok( - retryOperation( + await retryOperation( () => - new Promise((resolve, reject) => - logContents.match(/INFO hls:\s+Registering ide configuration/) !== null ? resolve : reject + new Promise((resolve, reject) => { + return getExtensionLogContent()?.match(/Registering IDE configuration/i) !== null + ? resolve(true) : reject(false); + } ), - 1000 * 5, - 20 + 1000 * 1, + 150 ), 'Extension log file has no hls output' ); }); - test('Server should inherit environment variables defined in the settings', async () => { + test('5. Server should inherit environment variables defined in the settings', async () => { await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); assert.ok( - retryOperation(() => new Promise((resolve, reject) => filesCreated.get('cache')!), 1000 * 5, 20), + await retryOperation( + () => + new Promise((resolve, reject) => { + return filesCreated.get('cache') ? resolve(true) : reject(false); + }), + 1000 * 5, + 10 + ), 'Server did not inherit XDG_CACHE_DIR from environment variables set in the settings' ); }); suiteTeardown(async () => { console.log('Disposing all resources'); - disposables.forEach((d) => d.dispose()); + disposables.forEach(d => d.dispose()); console.log('Stopping the lsp server'); await vscode.commands.executeCommand(StopServerCommandName); - await delay(5); + console.log('Contents of the extension log:'); const logContent = getExtensionLogContent(); if (logContent) { console.log(logContent); } + console.log('Deleting test workspace contents'); await deleteWorkspaceFiles([], (name) => !name.includes('.log')); }); diff --git a/test/suite/index.ts b/test/suite/index.ts index fd678705..f3dcf2c5 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: 90000, + timeout: 180_000, // 3 mins color: true, }); diff --git a/tsconfig.json b/tsconfig.json index 7b551f75..a1efbd37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,9 @@ "moduleResolution": "node", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": [ + "es6" + ], "sourceMap": true, "rootDir": ".", "noUnusedLocals": true, @@ -14,6 +16,13 @@ "noFallthroughCasesInSwitch": true, "strictNullChecks": true }, - "include": ["./src/**/*.ts", "./test/**/*.ts"], - "exclude": ["node_modules", ".vscode", ".vscode-test"] -} + "include": [ + "./src/**/*.ts", + "./test/**/*.ts" + ], + "exclude": [ + "node_modules", + ".vscode", + ".vscode-test" + ] +} \ No newline at end of file