aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers/lsp_wasm.js
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
commitecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch)
tree70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /tests/drivers/lsp_wasm.js
a
Diffstat (limited to 'tests/drivers/lsp_wasm.js')
-rw-r--r--tests/drivers/lsp_wasm.js112
1 files changed, 112 insertions, 0 deletions
diff --git a/tests/drivers/lsp_wasm.js b/tests/drivers/lsp_wasm.js
new file mode 100644
index 0000000..4ff2d16
--- /dev/null
+++ b/tests/drivers/lsp_wasm.js
@@ -0,0 +1,112 @@
+// tests for the language server as WebAssembly, through the extension's own
+// host (editors/vscode/client/wasmproc.js). the same protocol checks as the
+// native server, plus the hover docs of a compiler name.
+// usage: node tests/drivers/lsp_wasm.js path/to/lsp.wasm
+const path = require('path');
+const { wasmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'wasmproc.js'));
+const fs = require('fs');
+const wasm = process.argv[2];
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok lsp wasm ' + label); }
+ else { bad++; console.log('FAIL lsp wasm ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 300))); }
+}
+// runs one session: the messages are sent, and the answers collected by id
+function session(docs) {
+ const replies = {}, notes = [];
+ const p = wasmProcess(wasm, { log: () => {} });
+ const exited = { code: undefined };
+ p.on('exit', c => { exited.code = c; });
+ let inbox = Buffer.alloc(0);
+ p.stdout.on('data', d => {
+ inbox = Buffer.concat([inbox, d]);
+ for (;;) {
+ const e = inbox.indexOf('\r\n\r\n'); if (e < 0) return;
+ const n = parseInt(/Content-Length: (\d+)/.exec(inbox.slice(0, e).toString())[1], 10);
+ if (inbox.length < e + 4 + n) return;
+ const m = JSON.parse(inbox.slice(e + 4, e + 4 + n).toString('utf8'));
+ inbox = inbox.slice(e + 4 + n);
+ if (m.id !== undefined) replies[m.id] = m; else notes.push(m);
+ }
+ });
+ const send = m => { const b = Buffer.from(JSON.stringify(m), 'utf8'); p.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + b.length + '\r\n\r\n'), b])); };
+ send({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { processId: 1, capabilities: {} } });
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ for (const [uri, text] of docs) send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version: 1, text } } });
+ return { p, replies, notes, send, exited };
+}
+const clean = 'int main() {\n\tint x = 1;\n\treturn x;\n}\n';
+const bad_src = 'int main() {\n\tint x = 1\n\treturn x;\n}\n';
+const tokOf = (s, uri) => {
+ s.send({ jsonrpc: '2.0', id: 2, method: 'textDocument/semanticTokens/full', params: { textDocument: { uri } } });
+ s.send({ jsonrpc: '2.0', id: 3, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line: 1, character: 9 } } });
+ s.send({ jsonrpc: '2.0', id: 4, method: 'shutdown', params: null });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+};
+const diagsOf = (s, uri) => s.notes.filter(m => m.method === 'textDocument/publishDiagnostics' && m.params.uri === uri);
+
+// a clean program: initialize, no diagnostics, tokens, and it exits on its own
+{
+ const s = session([['file:///a.zc', clean]]);
+ tokOf(s, 'file:///a.zc');
+ const init = s.replies[1];
+ check(init && init.result.capabilities.semanticTokensProvider && init.result.capabilities.hoverProvider, 'initialize advertises tokens and hover', init);
+ check(diagsOf(s, 'file:///a.zc').length === 1 && diagsOf(s, 'file:///a.zc')[0].params.diagnostics.every(x => x.severity !== 1), 'a clean program has no errors (hints are fine)');
+ const t = s.replies[2];
+ check(t && t.result && t.result.data.length > 0 && t.result.data.length % 5 === 0, 'semantic tokens come back in groups of five', t && t.result && t.result.data.length);
+ check(s.replies[4] && s.replies[4].result === null, 'shutdown is answered');
+ check(s.p.stdin.write(Buffer.alloc(0)) === false, 'the server has exited after exit');
+}
+// a broken program: the compiler's own message is the diagnostic
+{
+ const s = session([['file:///b.zc', bad_src]]);
+ const d = diagsOf(s, 'file:///b.zc');
+ check(d.length === 1 && d[0].params.diagnostics.length > 0, 'a missing semicolon gives a diagnostic');
+ check(d.length === 1 && d[0].params.diagnostics.some(x => /expected/.test(x.message)), 'the message is the compiler\'s', d);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+}
+// hover: a name the document declares, and a compiler name with docs in the image
+{
+ const src = '// adds one to its argument\nint inc(int x) { return x + 1; }\nint main() {\n\treturn inc(2);\n}\n';
+ const s = session([['file:///c.zc', src]]);
+ s.send({ jsonrpc: '2.0', id: 5, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///c.zc' }, position: { line: 3, character: 9 } } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ const h = s.replies[5];
+ check(h && h.result && /adds one/.test(h.result.contents.value), 'hover shows the comment above a declaration', h);
+ const s2 = session([['file:///d.zc', 'int main() {\n\tint z = docofs("x");\n\treturn 0;\n}\n']]);
+ s2.send({ jsonrpc: '2.0', id: 6, method: 'textDocument/hover', params: { textDocument: { uri: 'file:///d.zc' }, position: { line: 1, character: 10 } } });
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ const h2 = s2.replies[6];
+ check(h2 && h2.result && /packed documentation/.test(h2.result.contents.value), 'hover shows the docs of a compiler name', h2);
+}
+// a document that is bigger than one feed: the input is split and comes out whole
+{
+ let big = 'int main() {\n';
+ for (let i = 0; i < 4000; i++) big += '\tint v' + i + ' = ' + i + ';\n';
+ big += '\treturn 0;\n}\n';
+ const s = session([['file:///e.zc', big]]);
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///e.zc').length === 1, 'a document bigger than one feed is read whole (' + big.length + ' bytes)');
+}
+// the regression: the server halted without an answer on any document that
+// needed more memory than its first heap (examples/bf.zc, and a document twice
+// the size of the earlier one). each must be checked and published, and the
+// server must still be running until it is told to exit
+{
+ const bfPath = path.join(__dirname, '..', '..', 'examples', 'bf.zc');
+ const bf = fs.readFileSync(bfPath, 'utf8');
+ const s = session([['file:///bf.zc', bf]]);
+ s.send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri: 'file:///bf.zc', version: 2 }, contentChanges: [{ text: bf }] } });
+ s.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s, 'file:///bf.zc').length >= 1, 'examples/bf.zc is checked and published (' + bf.length + ' bytes)');
+ check(s.exited.code === 0, 'the server runs until exit, then leaves with status 0', s.exited);
+ let huge = 'int main() {\n';
+ for (let i = 0; i < 12000; i++) huge += '\tint h' + i + ' = ' + i + ' + ' + (i % 7) + ';\n';
+ huge += '\treturn 0;\n}\n';
+ const s2 = session([['file:///huge.zc', huge]]);
+ s2.send({ jsonrpc: '2.0', method: 'exit' });
+ check(diagsOf(s2, 'file:///huge.zc').length === 1, 'a document past the first heap is checked and published (' + huge.length + ' bytes)', s2.exited);
+ check(s2.exited.code === 0, 'the big document leaves the server with status 0 after exit', s2.exited);
+}
+console.log(ok + ' lsp wasm checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);