Skip to content

Commit 67037d6

Browse files
committed
Test real webview messaging and server lifecycle
1 parent 496262e commit 67037d6

6 files changed

Lines changed: 133 additions & 8 deletions

File tree

‎client/src/extension.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<Liquid
4545
getState: () => ({
4646
status: extension.status,
4747
diagnostics: [...(extension.diagnostics ?? [])],
48+
serverPid: extension.serverProcess?.pid,
4849
}),
4950
onWebviewMessage: extension.webview!.onWebviewMessage,
5051
};

‎client/src/lsp/server.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export async function runLanguageServer(context: vscode.ExtensionContext, javaEx
2626
cwd: workspaceFolder ? normalizeFilePath(workspaceFolder.uri.fsPath) : context.extensionPath, // root path
2727
};
2828
extension.logger!.client.info("Creating language server process...");
29-
extension.serverProcess = child_process.spawn(javaExecutablePath, args, options);
29+
const serverProcess = child_process.spawn(javaExecutablePath, args, options);
30+
extension.serverProcess = serverProcess;
3031

3132
// listen to process events
3233
extension.serverProcess.stdout?.on("data", (data) => {
@@ -39,9 +40,9 @@ export async function runLanguageServer(context: vscode.ExtensionContext, javaEx
3940
extension.serverProcess.on("error", (err) => {
4041
extension.logger!.server.error(`Failed to start: ${err}`);
4142
});
42-
extension.serverProcess.on("close", (code) => {
43+
serverProcess.on("close", (code) => {
4344
extension.logger!.server.info(`Process exited with code ${code}`);
44-
extension.serverProcess = undefined;
45+
if (extension.serverProcess === serverProcess) extension.serverProcess = undefined;
4546
});
4647
return port;
4748
}

‎client/src/test/lifecycle.test.ts‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import * as assert from 'node:assert/strict';
2+
import * as path from 'node:path';
3+
import * as vscode from 'vscode';
4+
import type { LJDiagnostic } from '../types/diagnostics';
5+
import type { LJContext } from '../types/context';
6+
import type { LiquidJavaTestApi } from '../types/test-api';
7+
8+
suite('Bundled LiquidJava webview and lifecycle', () => {
9+
test('sends verification messages and verifies again after Stop, Start, and Restart', async () => {
10+
const installed = vscode.extensions.getExtension<LiquidJavaTestApi>('AlcidesFonseca.liquid-java');
11+
assert.ok(installed);
12+
const api = await installed.activate();
13+
await api.ready;
14+
15+
const workspace = vscode.workspace.workspaceFolders?.[0];
16+
assert.ok(workspace);
17+
const uri = vscode.Uri.joinPath(workspace.uri, 'src/main/java/FailingRefinement.java');
18+
const sameFile = (file: string) => vscode.Uri.file(path.resolve(file)).fsPath === uri.fsPath;
19+
const subscriptions: vscode.Disposable[] = [];
20+
const nextEvent = <T>(event: vscode.Event<T>, matches: (value: T) => boolean) =>
21+
new Promise<T>(resolve => {
22+
const subscription = event(value => {
23+
if (matches(value)) {
24+
subscription.dispose();
25+
resolve(value);
26+
}
27+
});
28+
subscriptions.push(subscription);
29+
});
30+
const isFixtureDiagnostic = (diagnostics: LJDiagnostic[]) => diagnostics.some(diagnostic =>
31+
diagnostic.type === 'refinement-error' && sameFile(diagnostic.file));
32+
const nextDiagnostics = () => nextEvent(api.onDiagnostics, isFixtureDiagnostic);
33+
const assertServerRunning = () => {
34+
const pid = api.getState().serverPid;
35+
assert.ok(pid, 'the bundled extension must expose its running Java server');
36+
assert.doesNotThrow(() => process.kill(pid, 0));
37+
return pid;
38+
};
39+
const assertServerStopped = (pid: number) => {
40+
assert.equal(api.getState().serverPid, undefined);
41+
assert.throws(() => process.kill(pid, 0), { code: 'ESRCH' }, 'Stop must terminate the Java process');
42+
assert.equal(api.getState().status, 'stopped');
43+
assert.deepEqual(api.getState().diagnostics, []);
44+
};
45+
46+
try {
47+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
48+
await vscode.commands.executeCommand('liquidjava.stop');
49+
await vscode.commands.executeCommand('liquidjava.start');
50+
const ready = nextEvent(api.onWebviewMessage, event =>
51+
event.direction === 'fromWebview' && event.message.type === 'ready');
52+
await vscode.commands.executeCommand('liquidjava.showView');
53+
await ready;
54+
55+
const initialDiagnostics = nextDiagnostics();
56+
const document = await vscode.workspace.openTextDocument(uri);
57+
await vscode.window.showTextDocument(document);
58+
await initialDiagnostics;
59+
60+
const diagnosticMessage = nextEvent(api.onWebviewMessage, event =>
61+
event.direction === 'toWebview' && event.message.type === 'diagnostics' &&
62+
isFixtureDiagnostic(event.message.diagnostics));
63+
const contextMessage = nextEvent(api.onWebviewMessage, event =>
64+
event.direction === 'toWebview' && event.message.type === 'context');
65+
const manualDiagnostics = nextDiagnostics();
66+
await vscode.commands.executeCommand('liquidjava.verify');
67+
assert.equal(api.getState().status, 'loading');
68+
const [diagnostics, outboundDiagnostics, outboundContext] = await Promise.all([
69+
manualDiagnostics, diagnosticMessage, contextMessage,
70+
]);
71+
assert.deepEqual(outboundDiagnostics.message.diagnostics, diagnostics);
72+
const context: LJContext = outboundContext.message.context;
73+
const valid = context.localVars.find(variable => variable.name === 'valid');
74+
assert.ok(valid, JSON.stringify(context));
75+
assert.ok(valid.position);
76+
assert.ok(sameFile(valid.position.file));
77+
assert.equal(valid.type, 'int');
78+
assert.ok(valid.mainRefinement.includes('> 0'));
79+
assert.equal(api.getState().status, 'failed');
80+
81+
const originalPid = assertServerRunning();
82+
const stopped = nextEvent(api.onWebviewMessage, event =>
83+
event.direction === 'toWebview' && event.message.type === 'status' && event.message.status === 'stopped');
84+
await vscode.commands.executeCommand('liquidjava.stop');
85+
await stopped;
86+
assertServerStopped(originalPid);
87+
88+
const startDiagnostics = nextDiagnostics();
89+
await vscode.commands.executeCommand('liquidjava.start');
90+
await startDiagnostics;
91+
const startedPid = assertServerRunning();
92+
assert.notEqual(startedPid, originalPid);
93+
assert.equal(api.getState().status, 'failed');
94+
95+
const restartStopped = nextEvent(api.onWebviewMessage, event =>
96+
event.direction === 'toWebview' && event.message.type === 'status' && event.message.status === 'stopped');
97+
const restartDiagnostics = nextDiagnostics();
98+
await vscode.commands.executeCommand('liquidjava.restart');
99+
await restartStopped;
100+
await restartDiagnostics;
101+
const restartedPid = assertServerRunning();
102+
assert.notEqual(restartedPid, startedPid);
103+
assert.throws(() => process.kill(startedPid, 0), { code: 'ESRCH' });
104+
105+
const finalDiagnostics = nextDiagnostics();
106+
await vscode.commands.executeCommand('liquidjava.verify');
107+
await finalDiagnostics;
108+
assert.equal(api.getState().status, 'failed');
109+
} finally {
110+
subscriptions.forEach(subscription => subscription.dispose());
111+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
112+
await vscode.commands.executeCommand('liquidjava.stop');
113+
await vscode.commands.executeCommand('liquidjava.start');
114+
}
115+
});
116+
});

‎client/src/types/test-api.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ export interface WebviewMessage {
1010
export interface LiquidJavaTestApi {
1111
readonly ready: Promise<void>;
1212
readonly onDiagnostics: vscode.Event<LJDiagnostic[]>;
13-
getState(): { status: ExtensionStatus | undefined; diagnostics: LJDiagnostic[] };
13+
getState(): { status: ExtensionStatus | undefined; diagnostics: LJDiagnostic[]; serverPid: number | undefined };
1414
readonly onWebviewMessage: vscode.Event<WebviewMessage>;
1515
}

‎client/src/utils/utils.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export async function connectToPort(
100100
*/
101101
export async function killProcess(proc?: child_process.ChildProcess) {
102102
return new Promise<void>((resolve, reject) => {
103-
if (!proc || proc.killed || proc.pid === undefined) {
103+
if (!proc || proc.exitCode !== null || proc.signalCode !== null || proc.pid === undefined) {
104104
// already killed
105105
resolve();
106106
return;
@@ -116,10 +116,12 @@ export async function killProcess(proc?: child_process.ChildProcess) {
116116
});
117117
} else {
118118
// Unix
119+
const onClose = () => resolve();
120+
proc.once("close", onClose);
119121
try {
120122
process.kill(proc.pid, "SIGKILL");
121-
resolve();
122123
} catch (err) {
124+
proc.removeListener("close", onClose);
123125
reject(err);
124126
}
125127
}
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import liquidjava.specification.Refinement;
22

33
public class FailingRefinement {
4-
@Refinement("_ > 0")
5-
int positive = -1;
4+
void check() {
5+
@Refinement("_ > 0")
6+
int valid = 1;
7+
8+
@Refinement("_ > 0")
9+
int positive = -1;
10+
}
611
}

0 commit comments

Comments
 (0)