Skip to content

Refactor tests to work correctly #872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 10, 2023
18 changes: 14 additions & 4 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
- name: 'Install `tree` for MacOs'
run: |
brew update
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,4 +598,4 @@
"which": "^3.0.0",
"yauzl": "^2.10.0"
}
}
}
272 changes: 140 additions & 132 deletions test/suite/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,200 +1,208 @@
// 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<any>) {
return Promise.race([f, delay(seconds)]);
}

const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));

const retryOperation = (operation: () => Promise<any>, delay: number, retries: number) =>
new Promise((resolve, reject): Promise<any> => {
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<string, Promise<vscode.Uri>> = 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<void> {
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<string, Promise<vscode.Uri>> = 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<vscode.Uri>((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<vscode.Uri>) {
return Promise.race([f, delay(seconds)]);
}

const wait = (ms: number) => new Promise(r => setTimeout(r, ms));

const retryOperation = (operation: () => Promise<boolean>, delay: number, retries: number) =>
new Promise((resolve, reject): Promise<void> => {
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);
});
});

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<void> {
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);

filesCreated.set('log', existsWorkspaceFile('hls.log'));
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 () => {
test('3. 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');
// 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 () => {
test('4. 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');
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/) !== null
? resolve(true) : reject(false);
}
),
1000 * 5,
20
10
),
'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'));
});
Expand Down
Loading