// include: a path is found next to the file that has it, then in the workspace folder, then as written; the // error for one that cannot be read is on its string; include std; needs no file; a server with no files of its own // is sent the text of each (zc/file). usage: node tests/drivers/include_test.js path/to/zclsp [path/to/lsp.wasm] const assert = require('assert'); const cp = require('child_process'); const fs = require('fs'); const os = require('os'); const path = require('path'); const server = path.resolve(process.argv[2]); const wasm = process.argv[3]; 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 parse(out) { const res = []; let d = out; while (d.length) { const e = d.indexOf('\r\n\r\n'), n = Number(/Content-Length:\s*(\d+)/i.exec(d.subarray(0, e).toString())[1]); res.push(JSON.parse(d.subarray(e + 4, e + 4 + n))); d = d.subarray(e + 4 + n); } return res; } // runs one session on the native server (cwd is where includes are read as written) or the wasm host function check(uri, text, { files = [], root = null, cwd = os.tmpdir(), host = 'native', navigation = false } = {}) { const msgs = [{ jsonrpc: '2.0', id: 1, method: 'initialize', params: { rootUri: root } }, ...files.map(([p, t]) => ({ jsonrpc: '2.0', method: 'zc/file', params: { path: p, text: t } })), { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } }, ...(navigation ? [2, 3].map(id => ({ jsonrpc: '2.0', id, method: 'textDocument/definition', params: { textDocument: { uri }, position: { line: 2, character: 20 } } })) : []), { jsonrpc: '2.0', method: 'exit' }]; let out; if (host === 'native') out = cp.spawnSync(server, [], { input: Buffer.concat(msgs.map(frame)), cwd, maxBuffer: 1 << 26 }).stdout; else { const { wasmProcess } = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'wasmproc.js')); const p = wasmProcess(path.resolve(wasm), { log: () => {} }); const chunks = []; p.stdout.on('data', d => chunks.push(d)); for (const m of msgs) p.stdin.write(frame(m)); out = Buffer.concat(chunks); } const replies = parse(out); if (navigation) { for (const id of [2, 3]) assert.deepStrictEqual(replies.find(m => m.id === id).result, [{ uri: 'file:///nowhere/proj/lib/virt.zc', range: { start: { line: 0, character: 4 }, end: { line: 0, character: 10 } } }], host + ': navigation uses client-provided includes repeatedly'); } const note = replies.filter(m => m.method === 'textDocument/publishDiagnostics').pop(); return note ? note.params.diagnostics : null; } const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'zcinc-')); const w = (f, t) => { fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true }); fs.writeFileSync(path.join(dir, f), t); }; w('src/lib/util.zc', 'int twice(int v) { return v * 2; }\n'); w('src/lib/deep.zc', 'include "util.zc";\nint quad(int v) { return twice(twice(v)); }\n'); w('src/tools/main.zc', 'include "../lib/deep.zc";\nfn main() { return quad(1); }\n'); w('top.zc', 'int fromtop() { return 7; }\n'); const uri = f => 'file://' + path.join(dir, f); const read = f => fs.readFileSync(path.join(dir, f), 'utf8'); const errors = d => d.filter(x => x.severity === 1); // next to the file, and nested includes next to the file that has them (the cwd is not the document's directory) assert.deepStrictEqual(errors(check(uri('src/tools/main.zc'), read('src/tools/main.zc'))), [], 'a relative include, nested'); // the original build.zc reproduction, checked from an unrelated working directory const buildFile = path.resolve(__dirname, '../../src/tools/build.zc'); const buildText = fs.readFileSync(buildFile, 'utf8').replace('include std;', 'include "../lib/std.zc";'); assert.deepStrictEqual(errors(check('file://' + buildFile, buildText)), [], 'build.zc finds ../lib/std.zc'); // the workspace folder assert.deepStrictEqual(errors(check(uri('src/tools/a.zc'), 'include "top.zc";\nfn main() { return fromtop(); }\n', { root: 'file://' + dir })), [], 'found in the workspace folder'); // as written, from the working directory assert.deepStrictEqual(errors(check(uri('src/tools/b.zc'), 'include "top.zc";\nfn main() { return fromtop(); }\n', { cwd: dir })), [], 'found from the working directory'); // an include that is not there: the error is on its string let d = check(uri('src/tools/c.zc'), 'include "../lib/nothere.zc";\nfn main() { return 0; }\n'); assert.strictEqual(errors(d).length, 1); assert.deepStrictEqual(errors(d)[0].range, { start: { line: 0, character: 8 }, end: { line: 0, character: 27 } }, 'the error is on the string: ' + JSON.stringify(errors(d)[0].range)); assert.match(errors(d)[0].message, /can't read that include/); // include std; and the same twice assert.deepStrictEqual(errors(check(uri('src/tools/d.zc'), 'include std;\ninclude std;\nfn main() { return std.Text.atoi("4"); }\n')), [], 'include std;'); d = check(uri('src/tools/e.zc'), 'include std\nfn main() { return 0; }\n'); assert.ok(errors(d).length >= 1, 'include std without a semicolon is an error'); d = check(uri('src/tools/f.zc'), 'include other;\nfn main() { return 0; }\n'); assert.match(errors(d)[0].message, /can't read that module include/); assert.deepStrictEqual(errors(d)[0].range.start, { line: 0, character: 8 }, 'an unknown library is reported at its name'); // findings inside an included file: hints are left out, an error is shown on the include with where it is w('src/lib/loud.zc', 'int a() { int unused = 1; return 2 * 2; }\nint b() { return 1 }\n'); d = check(uri('src/tools/g.zc'), 'include "../lib/loud.zc";\nfn main() { return a(); }\n'); assert.strictEqual(d.filter(x => x.severity !== 1).length, 0, 'no hints from a library: ' + JSON.stringify(d)); assert.strictEqual(errors(d).length, 1); assert.match(errors(d)[0].message, /^in .*loud\.zc, line 2: .*expected ';'/); assert.deepStrictEqual(errors(d)[0].range.start, { line: 0, character: 8 }); // a server that cannot read files is sent them const virtual = '/nowhere/proj/lib/virt.zc'; const vdoc = 'file:///nowhere/proj/app/main.zc'; const vtext = 'include "../lib/virt.zc";\ninclude "../lib/gone.zc";\nfn main() { return thrice(2); }\n'; const files = [[virtual, 'int thrice(int v) { return v * 3; }\n']]; for (const host of wasm ? ['native', 'wasm'] : ['native']) { d = check(vdoc, vtext, { files, host, navigation: true }); assert.strictEqual(errors(d).length, 1, host + ': ' + JSON.stringify(d)); assert.deepStrictEqual(errors(d)[0].range.start, { line: 1, character: 8 }, host + ': only the missing file is an error'); } fs.rmSync(dir, { recursive: true }); console.log('ok includes: found next to the file, in the workspace and as written; errors on the string; include std; files sent by the client');