diff options
Diffstat (limited to 'tests/drivers/lsp_vm.js')
| -rw-r--r-- | tests/drivers/lsp_vm.js | 112 |
1 files changed, 112 insertions, 0 deletions
diff --git a/tests/drivers/lsp_vm.js b/tests/drivers/lsp_vm.js new file mode 100644 index 0000000..24c1a5b --- /dev/null +++ b/tests/drivers/lsp_vm.js @@ -0,0 +1,112 @@ +// tests for the language server run by the javascript vm (src/js/tinyvm.js), through +// the extension's own host (editors/vscode/client/vmproc.js): the same checks as +// tests/drivers/lsp_wasm.js. the vm starts at the image's heap and grows as the server asks. +// usage: node tests/drivers/lsp_vm.js path/to/server/dir (tinyvm.js, lsp.json, layout.js) +const path = require('path'); +const { vmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'vmproc.js')); +const fs = require('fs'); +const dir = path.resolve(process.argv[2]); +let ok = 0, bad = 0; +function check(cond, label, extra) { + if (cond) { ok++; console.log('ok lsp vm ' + label); } + else { bad++; console.log('FAIL lsp vm ' + 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 = vmProcess(dir, { 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 vm checks passed, ' + bad + ' failed'); +process.exit(bad ? 1 : 0); |
