Skip to content

Commit 6296d15

Browse files
author
Fred Wu
committed
fix (test): HTML viewer tests and language server cleanup
1 parent d7df830 commit 6296d15

2 files changed

Lines changed: 77 additions & 4 deletions

File tree

‎src/test/suite/htmlWidgetViewer.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -848,14 +848,15 @@ suite('Session-aware HTML widget Viewer', () => {
848848
{ href: '../shared%20assets/', target: '/tmp/shared assets/', root: '/tmp/shared assets' },
849849
{ href: '', target: '/tmp/output/index.html', root: '/tmp/output' },
850850
{ href: vscode.Uri.file(path.resolve('/tmp/external/assets.html')).toString(), target: '/tmp/external/assets.html', root: '/tmp/external' },
851-
{ href: '/', target: path.parse(file).root, root: path.parse(file).root },
851+
{ href: '/', target: path.parse(file).root.replace(/\\/g, '/'), root: path.parse(file).root },
852852
{ href: 'data:text/plain,ignored', target: '/tmp/output/index.html', root: '/tmp/output' },
853853
{ href: 'https://[', target: '/tmp/output/index.html', root: '/tmp/output' },
854854
]) {
855855
read.resolves(`<!doctype html><html><head><base target="_self"><base HREF='${fixture.href}' data-authored="yes"><base href="ignored/"></head><body>Output</body></html>`);
856856
await showWebView(file, 'Authored base', 'Two');
857857
const panel = panels[panels.length - 1];
858-
const expected = vscode.Uri.file(path.resolve(fixture.target)).toString() + (fixture.target.endsWith('/') ? '/' : '');
858+
let expected = vscode.Uri.file(path.resolve(fixture.target)).toString();
859+
if (fixture.target.endsWith('/') && !expected.endsWith('/')) { expected += '/'; }
859860
assert.ok(panel.webview.html.includes(`<base href="${expected}" data-authored="yes">`), fixture.href);
860861
assert.ok(panel.webview.html.includes('<base target="_self">'));
861862
assert.ok(panel.webview.html.includes('<base href="ignored/">'));

‎src/test/suite/interactiveEditor.test.ts‎

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as assert from 'assert';
22
import * as fs from 'fs';
33
import * as os from 'os';
44
import * as path from 'path';
5+
import * as net from 'net';
56
import * as vscode from 'vscode';
67
import * as sinon from 'sinon';
78
import { execFile } from 'child_process';
@@ -18,7 +19,7 @@ import { shellQuote } from '../../interactive/launcher';
1819
import type { InteractiveManager } from '../../interactive/manager';
1920
import type { GlobalEnvItem, WorkspaceDataProvider } from '../../workspaceViewer';
2021
import type { WorkspaceData } from '../../session';
21-
import type { LanguageClient } from 'vscode-languageclient/node';
22+
import { LanguageClient, State, StreamMessageReader, StreamMessageWriter, createMessageConnection } from 'vscode-languageclient/node';
2223

2324
function treeTooltip(item: vscode.TreeItem): string {
2425
return typeof item.tooltip === 'string' ? item.tooltip : item.tooltip?.value ?? '';
@@ -85,6 +86,8 @@ function treeTooltip(item: vscode.TreeItem): string {
8586
const service = languageService();
8687
await Promise.all(service.clientUpdates.values());
8788
const clients = new Set([...service.clients.values()].filter(client => !originalLanguageClients.has(client)));
89+
// Await shutdown before tab-close handlers can start an unobserved stop.
90+
await Promise.all([...clients].map(stopLanguageClient));
8891
// Detach retains editors. Close this suite's tabs and await its language
8992
// servers before deleting directories that those R processes use as cwd.
9093
await vscode.window.tabGroups.close(vscode.window.tabGroups.all.flatMap(group => group.tabs)
@@ -96,7 +99,7 @@ function treeTooltip(item: vscode.TreeItem): string {
9699
service.clients.delete(key);
97100
}
98101
}
99-
await Promise.all([...clients].map(client => client.stop()));
102+
await Promise.all([...clients].map(stopLanguageClient));
100103
await new Promise(resolve => setTimeout(resolve, 200));
101104
fs.rmSync(root, { recursive: true, force: true });
102105
sourceDirectories.forEach(directory => fs.rmSync(directory, { recursive: true, force: true }));
@@ -2094,3 +2097,72 @@ par(mfrow=c(1,1))`);
20942097
});
20952098

20962099
});
2100+
2101+
async function stopLanguageClient(client: LanguageClient): Promise<void> {
2102+
try {
2103+
await client.stop();
2104+
} catch (error) {
2105+
// R's language server can close on shutdown before the client's exit
2106+
// notification is written. The client has already completed cleanup.
2107+
if (client.state === State.Stopped && error instanceof Error && 'code' in error && error.code === 'EPIPE') { return; }
2108+
throw error;
2109+
}
2110+
}
2111+
2112+
suite('Interactive language client cleanup', () => {
2113+
test('accepts a completed shutdown when the server closes before the exit notification', async () => {
2114+
const connections: Array<{ dispose(): void }> = [];
2115+
let serverSocket: net.Socket | undefined;
2116+
const server = net.createServer(socket => {
2117+
serverSocket = socket;
2118+
const connection = createMessageConnection(new StreamMessageReader(socket), new StreamMessageWriter(socket));
2119+
connections.push(connection);
2120+
connection.onRequest('initialize', () => ({ capabilities: {} }));
2121+
connection.onRequest('shutdown', () => null);
2122+
connection.listen();
2123+
});
2124+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
2125+
const socket = net.connect((server.address() as net.AddressInfo).port, '127.0.0.1');
2126+
const ended = new Promise<void>(resolve => socket.once('end', resolve));
2127+
const writer = new StreamMessageWriter(socket);
2128+
const write = writer.write.bind(writer);
2129+
let exitErrorCode: string | undefined;
2130+
// Force the same ordering that macOS can produce during R shutdown.
2131+
writer.write = async message => {
2132+
const isExit = 'method' in message && message.method === 'exit';
2133+
if (isExit) {
2134+
serverSocket!.end();
2135+
await ended;
2136+
}
2137+
try {
2138+
return await write(message);
2139+
} catch (error) {
2140+
if (isExit) { exitErrorCode = (error as NodeJS.ErrnoException).code; }
2141+
throw error;
2142+
}
2143+
};
2144+
const channel = vscode.window.createOutputChannel('Test language client cleanup', { log: true });
2145+
const client = new LanguageClient('cleanup-test', 'Cleanup test', () => Promise.resolve({
2146+
reader: new StreamMessageReader(socket), writer,
2147+
}), { documentSelector: [], outputChannel: channel });
2148+
try {
2149+
await client.start();
2150+
await stopLanguageClient(client);
2151+
assert.strictEqual(client.state, State.Stopped);
2152+
assert.strictEqual(exitErrorCode, 'EPIPE');
2153+
} finally {
2154+
writer.dispose(); socket.destroy();
2155+
connections.forEach(connection => connection.dispose());
2156+
await new Promise<void>(resolve => server.close(() => resolve()));
2157+
channel.dispose();
2158+
}
2159+
});
2160+
2161+
test('does not suppress unrelated shutdown failures or an unstopped client', async () => {
2162+
const error = Object.assign(new Error('Socket shutdown failed'), { code: 'EPIPE' });
2163+
for (const [state, failure] of [[State.Running, error], [State.Stopped, new Error('Unexpected shutdown failure')]] as const) {
2164+
const client = { state, stop: () => Promise.reject(failure) } as unknown as LanguageClient;
2165+
await assert.rejects(stopLanguageClient(client), thrown => thrown === failure);
2166+
}
2167+
});
2168+
});

0 commit comments

Comments
 (0)