1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
// 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);
}
// definition navigation returns the embedded runtime source through the wasm host.
{
const src = 'include rt; fn main() { return rt.s_to_n(b"42"); }\n';
const s = session([['file:///nav.zc', src]]);
const at = src.indexOf('s_to_n'), before = src.slice(0, at);
s.send({ jsonrpc: '2.0', id: 7, method: 'textDocument/definition', params: { textDocument: { uri: 'file:///nav.zc' }, position: { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 } } });
s.send({ jsonrpc: '2.0', id: 8, method: 'zc/source', params: { path: '/rt.zc' } });
s.send({ jsonrpc: '2.0', method: 'exit' });
const loc = s.replies[7] && s.replies[7].result && s.replies[7].result[0];
check(loc && loc.uri === 'tinyvm:/rt.zc', 's_to_n opens the embedded runtime source', loc);
const source = s.replies[8] && s.replies[8].result;
const line = source && loc && source.split('\n')[loc.range.start.line];
check(line && line.slice(loc.range.start.character, loc.range.end.character) === 's_to_n' && source === fs.readFileSync(path.join(__dirname, '..', '..', 'src', 'runtime', 'rt.zc'), 'utf8'), 'the virtual runtime source matches its declaration');
}
// 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);
|