Skip to content

Commit 627b1da

Browse files
authored
Merge pull request #496 from jneira/fixci
Fix ci: ensure we have a supported ghc version in PATH
2 parents 1a9ad3c + 62d3209 commit 627b1da

File tree

7 files changed

+123
-35
lines changed

7 files changed

+123
-35
lines changed

.github/workflows/build.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ jobs:
2424
uses: HaaLeo/publish-vscode-extension@v0
2525
with:
2626
pat: ${{ secrets.OPEN_VSX_TOKEN }}
27-
- name: Upload extension vsix
27+
- name: Upload extension vsix to workflow artifacts
28+
uses: actions/upload-artifact@v2
29+
with:
30+
name: haskell-${{ github.event.release.tag_name }}.vsix
31+
path: ${{ steps.publishToVSMarketplace.outputs.vsixPath }}
32+
- name: Upload extension vsix to release assets
2833
uses: actions/[email protected]
2934
env:
3035
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@ jobs:
1919
uses: actions/setup-node@v1
2020
with:
2121
node-version: 10.x
22+
- name: Ensure there is a supported ghc versions
23+
uses: haskell/actions/setup@v1
24+
with:
25+
ghc-version: 9.0.1
2226
- run: npm ci
2327
- run: npm run webpack
24-
- run: xvfb-run -a npm test
28+
- run: xvfb-run -s '-screen 0 640x480x16' -a npm test
2529
if: runner.os == 'Linux'
2630
- run: npm test
2731
if: runner.os != 'Linux'
32+
- name: Upload log file to workflow artifacts on error
33+
if: failure()
34+
uses: actions/upload-artifact@v2
35+
with:
36+
name: extension-${{ matrix.os }}.log
37+
path: test-workspace/hls.log
38+

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@
109109
"enum": [
110110
"off",
111111
"error",
112+
"info",
112113
"debug"
113114
],
114-
"default": "error",
115+
"default": "info",
115116
"description": "Sets the log level in the client side."
116117
},
117118
"haskell.logFile": {
@@ -398,7 +399,7 @@
398399
"@types/request-promise-native": "^1.0.17",
399400
"@types/vscode": "^1.52.0",
400401
"@types/yauzl": "^2.9.1",
401-
"@vscode/test-electron": "^1.6.1",
402+
"@vscode/test-electron": "^1.6.2",
402403
"glob": "^7.1.4",
403404
"husky": "^7.0.2",
404405
"mocha": "^9.1.2",

src/extension.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function findManualExecutable(logger: Logger, uri: Uri, folder?: WorkspaceFolder
108108
}
109109
logger.info(`Trying to find the server executable in: ${exePath}`);
110110
exePath = resolvePathPlaceHolders(exePath, folder);
111-
logger.info(`Location after path variables substitution: ${exePath}`);
111+
logger.log(`Location after path variables substitution: ${exePath}`);
112112

113113
if (!executableExists(exePath)) {
114114
let msg = `serverExecutablePath is set to ${exePath}`;
@@ -164,6 +164,8 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
164164
return;
165165
}
166166

167+
const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath);
168+
167169
// Set the key to null to prevent multiple servers being launched at once
168170
clients.set(clientsKey, null);
169171

@@ -173,11 +175,14 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
173175

174176
const outputChannel: OutputChannel = window.createOutputChannel(langName);
175177

176-
const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel);
177-
178-
logger.info('Environment variables:');
178+
const logFilePath = logFile ? path.resolve(currentWorkingDir, logFile) : undefined;
179+
const logger: Logger = new ExtensionLogger('client', clientLogLevel, outputChannel, logFilePath);
180+
if (logFilePath) {
181+
logger.info(`Writing client log to file ${logFilePath}`);
182+
}
183+
logger.log('Environment variables:');
179184
Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => {
180-
logger.info(` ${key}: ${value}`);
185+
logger.log(` ${key}: ${value}`);
181186
});
182187

183188
let serverExecutable;
@@ -217,11 +222,13 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
217222
// If we're operating on a standalone file (i.e. not in a folder) then we need
218223
// to launch the server in a reasonable current directory. Otherwise the cradle
219224
// guessing logic in hie-bios will be wrong!
225+
let cwdMsg = `Activating the language server in working dir: ${currentWorkingDir}`;
220226
if (folder) {
221-
logger.info(`Activating the language server in the workspace folder: ${folder?.uri.fsPath}`);
227+
cwdMsg += ' (the workspace folder)';
222228
} else {
223-
logger.info(`Activating the language server in the parent dir of the file: ${uri.fsPath}`);
229+
cwdMsg += ` (parent dir of loaded file ${uri.fsPath})`;
224230
}
231+
logger.info(cwdMsg);
225232

226233
const serverEnvironment: IEnvVars = workspace.getConfiguration('haskell', uri).serverEnvironment;
227234
const exeOptions: ExecutableOptions = {
@@ -252,7 +259,7 @@ async function activateServerForFolder(context: ExtensionContext, uri: Uri, fold
252259
}
253260

254261
const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*';
255-
logger.info(`document selector patten: ${pat}`);
262+
logger.log(`document selector patten: ${pat}`);
256263
const clientOptions: LanguageClientOptions = {
257264
// Use the document selector to only notify the LSP on files inside the folder
258265
// path for the specific workspace.

src/utils.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,19 @@ enum LogLevel {
1818
Error,
1919
Warn,
2020
Info,
21+
Debug
2122
}
2223
export class ExtensionLogger implements Logger {
2324
public readonly name: string;
2425
public readonly level: LogLevel;
2526
public readonly channel: OutputChannel;
27+
public readonly logFile: string | undefined;
2628

27-
constructor(name: string, level: string, channel: OutputChannel) {
29+
constructor(name: string, level: string, channel: OutputChannel, logFile: string | undefined) {
2830
this.name = name;
2931
this.level = this.getLogLevel(level);
3032
this.channel = channel;
33+
this.logFile = logFile;
3134
}
3235
public warn(message: string): void {
3336
this.logLevel(LogLevel.Warn, message);
@@ -41,13 +44,25 @@ export class ExtensionLogger implements Logger {
4144
this.logLevel(LogLevel.Error, message);
4245
}
4346

44-
public log(msg: string) {
45-
this.channel.appendLine(msg);
47+
public log(message: string) {
48+
this.logLevel(LogLevel.Debug, message);
49+
}
50+
51+
private write(msg: string) {
52+
let now = new Date();
53+
// Ugly hack to make js date iso format similar to hls one
54+
const offset = now.getTimezoneOffset();
55+
now = new Date(now.getTime() - (offset * 60 * 1000));
56+
const timedMsg = `${new Date().toISOString().replace('T', ' ').replace('Z', '0000')} ${msg}`;
57+
this.channel.appendLine(timedMsg);
58+
if (this.logFile) {
59+
fs.appendFileSync(this.logFile, timedMsg + '\n');
60+
}
4661
}
4762

4863
private logLevel(level: LogLevel, msg: string) {
4964
if (level <= this.level) {
50-
this.log(`[${this.name}][${LogLevel[level].toUpperCase()}] ${msg}`);
65+
this.write(`[${this.name}] ${LogLevel[level].toUpperCase()} ${msg}`);
5166
}
5267
}
5368

@@ -57,6 +72,8 @@ export class ExtensionLogger implements Logger {
5772
return LogLevel.Off;
5873
case 'error':
5974
return LogLevel.Error;
75+
case 'debug':
76+
return LogLevel.Debug;
6077
default:
6178
return LogLevel.Info;
6279
}

test/runTest.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// tslint:disable: no-console
12
import * as cp from 'child_process';
23
import * as fs from 'fs';
34
import * as path from 'path';
@@ -28,6 +29,7 @@ async function main() {
2829
const extensionTestsPath = path.resolve(__dirname, './suite/index');
2930

3031
const testWorkspace = path.resolve(__dirname, '../../test-workspace');
32+
console.log(`Test workspace: ${testWorkspace}`);
3133

3234
if (!fs.existsSync(testWorkspace)) {
3335
fs.mkdirSync(testWorkspace);

test/suite/extension.test.ts

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// tslint:disable: no-console
22
import * as assert from 'assert';
3+
import * as fs from 'fs';
34
import * as os from 'os';
45
import * as path from 'path';
56
import { TextEncoder } from 'util';
@@ -10,12 +11,12 @@ function getExtension() {
1011
return vscode.extensions.getExtension('haskell.haskell');
1112
}
1213

13-
async function delay(ms: number) {
14-
return new Promise((resolve) => setTimeout(() => resolve(false), ms));
14+
async function delay(seconds: number) {
15+
return new Promise((resolve) => setTimeout(() => resolve(false), seconds * 1000));
1516
}
1617

1718
async function withTimeout(seconds: number, f: Promise<any>) {
18-
return Promise.race([f, delay(seconds * 1000)]);
19+
return Promise.race([f, delay(seconds)]);
1920
}
2021

2122
function getHaskellConfig() {
@@ -31,18 +32,39 @@ function getWorkspaceFile(name: string) {
3132
return wsroot.with({ path: path.posix.join(wsroot.path, name) });
3233
}
3334

34-
async function deleteWorkspaceFiles() {
35-
const dirContents = await vscode.workspace.fs.readDirectory(getWorkspaceRoot().uri);
36-
console.log(`Deleting test ws contents: ${dirContents}`);
35+
async function deleteWorkspaceFiles(pred?: (fileType: [string, vscode.FileType]) => boolean) {
36+
await deleteFiles(getWorkspaceRoot().uri, pred);
37+
}
38+
39+
function getExtensionLogContent(): string | undefined {
40+
const extLog = getWorkspaceFile('hls.log').fsPath;
41+
if (fs.existsSync(extLog)) {
42+
const logContents = fs.readFileSync(extLog);
43+
return logContents.toString();
44+
} else {
45+
console.log(`${extLog} does not exist!`);
46+
return undefined;
47+
}
48+
}
49+
50+
async function deleteFiles(dir: vscode.Uri, pred?: (fileType: [string, vscode.FileType]) => boolean) {
51+
const dirContents = await vscode.workspace.fs.readDirectory(dir);
52+
console.log(`Deleting ${dir} contents: ${dirContents}`);
3753
dirContents.forEach(async ([name, type]) => {
3854
const uri: vscode.Uri = getWorkspaceFile(name);
39-
console.log(`Deleting ${uri}`);
40-
await vscode.workspace.fs.delete(getWorkspaceFile(name), { recursive: true });
55+
if (!pred || pred([name, type])) {
56+
console.log(`Deleting ${uri}`);
57+
await vscode.workspace.fs.delete(getWorkspaceFile(name), {
58+
recursive: true,
59+
useTrash: false,
60+
});
61+
}
4162
});
4263
}
4364

4465
suite('Extension Test Suite', () => {
4566
const disposables: vscode.Disposable[] = [];
67+
const filesCreated: Map<string, Promise<vscode.Uri>> = new Map();
4668

4769
async function existsWorkspaceFile(pattern: string, pred?: (uri: vscode.Uri) => boolean) {
4870
const relPath: vscode.RelativePattern = new vscode.RelativePattern(getWorkspaceRoot(), pattern);
@@ -65,9 +87,19 @@ suite('Extension Test Suite', () => {
6587
await getHaskellConfig().update('logFile', 'hls.log');
6688
await getHaskellConfig().update('trace.server', 'messages');
6789
await getHaskellConfig().update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile('bin').fsPath));
68-
await getHaskellConfig().update('serverEnvironment', { XDG_CACHE_HOME: path.normalize(getWorkspaceFile('cache-test').fsPath) });
90+
await getHaskellConfig().update('serverEnvironment', {
91+
XDG_CACHE_HOME: path.normalize(getWorkspaceFile('cache-test').fsPath),
92+
});
6993
const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"');
7094
await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents);
95+
96+
const pred = (uri: vscode.Uri) => !['download', 'gz', 'zip'].includes(path.extname(uri.fsPath));
97+
const exeExt = os.platform.toString() === 'win32' ? '.exe' : '';
98+
// Setting up watchers before actual tests start, to ensure we will got the created event
99+
filesCreated.set('wrapper', existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}`, pred));
100+
filesCreated.set('server', existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}`, pred));
101+
filesCreated.set('log', existsWorkspaceFile('hls.log'));
102+
filesCreated.set('cache', existsWorkspaceFile('cache-test'));
71103
});
72104

73105
test('Extension should be present', () => {
@@ -79,40 +111,53 @@ suite('Extension Test Suite', () => {
79111
assert.ok(true);
80112
});
81113

114+
test('Extension should create the extension log file', async () => {
115+
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
116+
assert.ok(await withTimeout(30, filesCreated.get('log')!), 'Extension log not created in 30 seconds');
117+
});
118+
82119
test('HLS executables should be downloaded', async () => {
83120
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
84-
const exeExt = os.platform.toString() === 'win32' ? '.exe' : '';
85121
console.log('Testing wrapper');
86-
const pred = (uri: vscode.Uri) => !['download', 'gz', 'zip'].includes(path.extname(uri.fsPath));
87122
assert.ok(
88-
await withTimeout(30, existsWorkspaceFile(`bin/haskell-language-server-wrapper*${exeExt}`, pred)),
123+
await withTimeout(30, filesCreated.get('wrapper')!),
89124
'The wrapper executable was not downloaded in 30 seconds'
90125
);
91126
console.log('Testing server');
92127
assert.ok(
93-
await withTimeout(60, existsWorkspaceFile(`bin/haskell-language-server-[1-9]*${exeExt}`, pred)),
128+
await withTimeout(60, filesCreated.get('server')!),
94129
'The server executable was not downloaded in 60 seconds'
95130
);
96131
});
97132

98-
test('Server log should be created', async () => {
133+
test('Extension log should have server output', async () => {
99134
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
100-
assert.ok(await withTimeout(30, existsWorkspaceFile('hls.log')), 'Server log not created in 30 seconds');
135+
await delay(10);
136+
const logContents = getExtensionLogContent();
137+
assert.ok(logContents, 'Extension log file does not exist');
138+
assert.match(logContents, /INFO hls:\s+Registering ide configuration/, 'Extension log file has no hls output');
101139
});
102140

103141
test('Server should inherit environment variables defined in the settings', async () => {
104142
await vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
105143
assert.ok(
106-
// Folder will have already been created by this point, so it will not trigger watcher in existsWorkspaceFile()
107-
vscode.workspace.getWorkspaceFolder(getWorkspaceFile('cache-test')),
144+
await withTimeout(30, filesCreated.get('cache')!),
108145
'Server did not inherit XDG_CACHE_DIR from environment variables set in the settings'
109146
);
110147
});
111148

112149
suiteTeardown(async () => {
150+
console.log('Disposing all resources');
113151
disposables.forEach((d) => d.dispose());
152+
console.log('Stopping the lsp server');
114153
await vscode.commands.executeCommand(CommandNames.StopServerCommandName);
115-
delay(5); // to give time to shutdown server
116-
await deleteWorkspaceFiles();
154+
await delay(5);
155+
console.log('Contents of the extension log:');
156+
const logContent = getExtensionLogContent();
157+
if (logContent) {
158+
console.log(logContent);
159+
}
160+
console.log('Deleting test workspace contents');
161+
await deleteWorkspaceFiles(([name, type]) => !name.includes('.log'));
117162
});
118163
});

0 commit comments

Comments
 (0)