diff options
Diffstat (limited to 'tests/drivers/completion_test.js')
| -rw-r--r-- | tests/drivers/completion_test.js | 105 |
1 files changed, 105 insertions, 0 deletions
diff --git a/tests/drivers/completion_test.js b/tests/drivers/completion_test.js new file mode 100644 index 0000000..22c5fa5 --- /dev/null +++ b/tests/drivers/completion_test.js @@ -0,0 +1,105 @@ +// compiler-backed completion, including incomplete source and position-aware lifetimes. +// usage: node tests/drivers/completion_test.js path/to/zclsp [server arguments] +const assert = require('assert'); +const cp = require('child_process'); +const path = require('path'); +const uri = 'file://' + path.resolve('tests/drivers/completion-fixture.zc'); +const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; +function complete(source, extra = []) { + const at = source.indexOf('|'); assert.ok(at >= 0); + const before = source.slice(0, at), text = source.replace('|', ''); + const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 }; + const msgs = [ + { id: 1, method: 'initialize', params: {} }, + { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }, + { id: 2, method: 'textDocument/completion', params: { textDocument: { uri }, position } }, + ...extra.map(m => ({ ...m, params: { textDocument: { uri }, position, ...m.params } })), + { method: 'exit' } + ].map(m => ({ jsonrpc: '2.0', ...m })); + let output; + if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') { + const wasm = process.argv[2] === '--wasm'; + const host = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js')); + const proc = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} }); + const chunks = []; proc.stdout.on('data', b => chunks.push(b)); + for (const m of msgs) proc.stdin.write(frame(m)); + output = Buffer.concat(chunks); + } else { + const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000, maxBuffer: 32 << 20 }); + assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout; + } + let b = output; const replies = []; + while (b.length) { + const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0, b.toString()); + const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]); + replies.push(JSON.parse(b.subarray(e + 4, e + 4 + n))); b = b.subarray(e + 4 + n); + } + assert.ok(replies.find(m => m.id === 1).result.capabilities.completionProvider); + const reply = replies.find(m => m.id === 2); assert.ok(reply && reply.result, JSON.stringify(reply)); + return { items: reply.result.items, replies }; +} +const get = (items, label) => { const item = items.find(c => c.label === label); assert.ok(item, `${label} missing: ${items.map(c => c.label)}`); return item; }; +let r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() {\n int tally = 3;\n ta|\n}\n'); +assert.match(get(r.items, 'tally').detail, /int tally/); +assert.ok(!r.items.some(c => c.label === 'count'), 'other function parameters are out of scope'); +r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() { to| }'); +assert.match(get(r.items, 'total').detail, /int total\(int count\)/); +assert.match(get(r.items, 'total').documentation.value, /computes a total/); +r = complete('fn main() {\n int count = 1;\n { int hidden = 3; }\n |\n}\n'); +get(r.items, 'count'); assert.ok(!r.items.some(c => c.label === 'hidden')); +r = complete('include rt; fn main(int input) {\n let p = rt.new(int, 3);\n rt.free(p);\n |\n}\n'); +const p = get(r.items, 'p'); assert.deepStrictEqual(p.tags, [1]); +assert.match(p.documentation.value, /lifetime ended/); +get(r.items, 'input'); +assert.ok(r.items.findIndex(c => c.label === 'p') > r.items.findIndex(c => c.label === 'input')); +assert.ok(r.items.slice(r.items.findIndex(c => c.tags)).every(c => c.tags), 'all consumed variables sort last'); +r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n p|;\n rt.free(p);\n return 0;\n}'); +assert.ok(!get(r.items, 'p').tags, 'later consumption does not expire earlier completion'); +r = complete('fn main() {\n int old = 1;\n int bold = 2;\n int other_local_data = 3;\n old|\n}'); +assert.strictEqual(r.items[0].label, 'old', 'exact match comes first'); get(r.items, 'other_local_data'); +r = complete('include std;\nfn main() { std.Text.at| }'); +assert.match(get(r.items, 'atoi').detail, /atoi\(/); +assert.ok(r.items.every(c => !c.label.includes('.')), 'member labels omit receiver'); +r = complete('struct point { int x; int y; };\nfn main() { point p; p.| }'); +get(r.items, 'x'); get(r.items, 'y'); assert.ok(!r.items.some(c => c.label === 'main')); +r = complete('fn main() { Te| }'); assert.ok(!r.items.some(c => c.label === 'Text'), 'std is not imported automatically'); +r = complete('int shared;\nfn main() { char shared = 1; sh| }'); +assert.match(get(r.items, 'shared').detail, /char shared/); +assert.strictEqual(r.items.filter(c => c.label === 'shared').length, 1); +r = complete('fn main() { let hello = "hi"; hel| }'); +assert.match(get(r.items, 'hello').detail, /char\* hello/); +r = complete('fn main() { let bytes = b"hi"; byt| }'); +assert.match(get(r.items, 'bytes').detail, /bstr bytes/); +r = complete('fn main() { int count = 1; cou|nt; return count; }', [{ id: 3, method: 'textDocument/hover' }]); +assert.ok(r.replies.find(m => m.id === 3).result, 'hover still works after a partial completion check'); +assert.strictEqual(get(r.items, 'count').textEdit.newText, 'count'); +assert.strictEqual(get(r.items, 'count').textEdit.range.end.character - get(r.items, 'count').textEdit.range.start.character, 5); +r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n let alias = p;\n rt.free(p);\n |\n}'); +assert.deepStrictEqual(get(r.items, 'alias').tags, [1], 'freed aliases are also unavailable'); +r = complete('include rt; fn take(.d p) { rt.free(p); }\nfn main() {\n let p = rt.new(int, 3);\n take(d*p);\n |\n}'); +assert.deepStrictEqual(get(r.items, 'p').tags, [1], 'ownership transfers are unavailable'); +r = complete('fn main() {\n int outer = 1;\n let f = fn(int inner) {\n ou|\n };\n}'); +get(r.items, 'outer'); +r = complete('fn first() { int wrong = 1; return wrong; }\n|\nfn second() { int other = 2; return other; }'); +assert.ok(!r.items.some(c => c.label === 'wrong' || c.label === 'other'), 'top-level completion excludes function locals'); +r = complete('fn main() {\n /* the latest count */\n int latest = 1;\n la|'); +assert.match(get(r.items, 'latest').documentation.value, /the latest count/); +r = complete('include rt; fn main() { int[?] items = {}; items.| }'); +for (const name of ['len', 'cap', 'push', 'pop', 'resize']) get(r.items, name); +assert.match(get(r.items, 'push').detail, /push\(int value\)/); +assert.match(get(r.items, 'pop').detail, /int pop\(\)/); +assert.match(get(r.items, 'resize').documentation.value, /length/); +assert.ok(!r.items.some(c => c.label === 'main'), 'array member completion excludes globals'); +r = complete('fn main() { int[3] items = {1,2,3}; items.| }'); +get(r.items, 'len'); get(r.items, 'cap'); +assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed arrays omit mutation methods'); +r = complete('include rt; fn main() { fixed int[?] items = rt.new(fixed int[?], 3); items.| }'); +get(r.items, 'len'); get(r.items, 'cap'); +assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed handles omit mutation methods'); +r = complete('include std; fn main() { int[3] items = {1,2,3}; items.| }'); +for (const name of ['find', 'contains', 'find_if', 'any', 'all', 'count', 'map', 'filter', 'reduce']) get(r.items, name); +assert.match(get(r.items, 'map').documentation.value, /transform.*each element/); +assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'library algorithms do not make a fixed array growable'); +r = complete('include rt; fn main() { int[?] items = {}; items.| }'); +assert.ok(!r.items.some(c => ['map', 'filter'].includes(c.label)), 'library methods require an explicit import'); +console.log('ok completion: signatures, docs, fuzzy ranking, scope, array members, shadowing and consumed lifetimes'); |
