// the rules of the diagnostics cache (editors/vscode/client/diagcache.js), with // plain node and no vscode: a key covers the server build, the text, and every // file the text reads through include, transitively. // // node tests/drivers/diagcache_test.js const fs = require('fs'); const os = require('os'); const path = require('path'); const assert = require('assert'); const dc = require(path.join(__dirname, '..', '..', 'editors', 'vscode', 'client', 'diagcache.js')); let passed = 0, failed = 0; function check(name, fn) { try { fn(); passed++; console.log('ok ' + name); } catch (e) { failed++; console.log('FAIL ' + name + ': ' + e.message); } } const root = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-diagcache-')); const write = (rel, text) => { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); return p; }; const A = write('a.zc', 'include "b.zc";\nfn main() { return helper(); }\n'); const B = write('b.zc', 'include "c.zc";\n'); const C = write('c.zc', 'fn helper() { return 1; }\n'); const U = write('unrelated.zc', 'fn other() { return 2; }\n'); const aText = fs.readFileSync(A, 'utf8'); const keyA = (stamp = 'S1', text = aText, p = A) => dc.diagKey(stamp, text, p, root).key; check('includeNames reads every include, however it is spaced', () => { assert.deepStrictEqual(dc.includeNames('include "a.zc";\ninclude"b/c.zc";\nfn f() {}'), ['a.zc', 'b/c.zc']); assert.deepStrictEqual(dc.includeNames('fn f() { return 1; }'), []); }); check('includeNames ignores comments and literals while allowing comments between tokens', () => { assert.deepStrictEqual(dc.includeNames('// include "fake.zc"\n/* include "also-fake.zc" */\nlet s = "include \\"string.zc\\"";\ninclude /* dependency */ "real.zc";'), ['real.zc']); }); check('the same tree gives the same key', () => { assert.strictEqual(keyA(), keyA()); assert.match(keyA(), /^[0-9a-f]{64}$/); }); check('a change to the document text changes the key', () => { assert.notStrictEqual(keyA('S1', aText + ' '), keyA()); }); check('a change to the server build changes the key', () => { assert.notStrictEqual(keyA('S2'), keyA('S1')); }); check('a change to a file the document includes, two steps down, changes the key', () => { const before = keyA(); fs.writeFileSync(C, 'fn helper() { return 3; }\n'); const after = keyA(); assert.notStrictEqual(before, after); fs.writeFileSync(C, 'fn helper() { return 1; }\n'); assert.strictEqual(keyA(), before, 'putting it back gives the old key again'); }); check('a file nothing includes does not change the key', () => { const before = keyA(); fs.writeFileSync(U, 'fn other() { return 9; }\n'); assert.strictEqual(keyA(), before); }); check('creating a missing include changes the key', () => { const M = write('m.zc', 'include "later.zc";\nfn f() { return 0; }\n'); const mText = fs.readFileSync(M, 'utf8'); const before = dc.diagKey('S1', mText, M, root); assert.ok(before.deps.some(p => p.endsWith('later.zc')), 'the missing name is still a dependency'); write('later.zc', 'fn g() { return 1; }\n'); assert.notStrictEqual(dc.diagKey('S1', mText, M, root).key, before.key); }); check('an include is found from the workspace folder as well as the including file', () => { const X = write('sub/x.zc', 'include "b.zc";\nfn x() { return 0; }\n'); const tree = dc.sourceTree(fs.readFileSync(X, 'utf8'), X, root); assert.ok(tree.files.has(path.join(root, 'b.zc')), 'b.zc is read from the workspace root'); assert.ok(tree.files.has(path.join(root, 'sub', 'b.zc')), 'and the directory of x.zc is tried too'); assert.ok(tree.complete); }); check('a tree too large to cover has no key, so nothing is cached for it', () => { let many = ''; for (let i = 0; i < dc.MAX_FILES + 5; i++) many += 'include "nope' + i + '.zc";\n'; const big = write('big.zc', many); const r = dc.diagKey('S1', many, big, root); assert.strictEqual(r.key, null); const cache = new dc.DiagCache(null); assert.strictEqual(cache.put(r.key, []), false); assert.strictEqual(cache.size(), 0); }); check('the cache answers only for the key it was given', () => { const cache = new dc.DiagCache(null); const d = [{ message: 'x' }]; assert.ok(cache.put('k1', d)); assert.deepStrictEqual(cache.get('k1'), d); assert.strictEqual(cache.get('k2'), null); assert.strictEqual(cache.get(null), null); assert.strictEqual(cache.put(null, []), false, 'a tree without a key is not kept'); assert.strictEqual(cache.size(), 1); }); check('two states of one document are both kept, so going back is a hit', () => { const cache = new dc.DiagCache(null); cache.put('state-a', []); cache.put('state-b', [{ message: 'b' }]); assert.deepStrictEqual(cache.get('state-a'), []); assert.deepStrictEqual(cache.get('state-b'), [{ message: 'b' }]); }); check('the least recently used entries go first past the limit', () => { const cache = new dc.DiagCache(null, 2); cache.put('1', []); cache.put('2', []); cache.get('1'); cache.put('3', []); assert.strictEqual(cache.size(), 2); assert.strictEqual(cache.get('2'), null, 'two was the least recently used'); assert.deepStrictEqual(cache.get('1'), []); assert.deepStrictEqual(cache.get('3'), []); }); check('the cache carries over a save and load as plain JSON', () => { const cache = new dc.DiagCache(null); cache.put('k1', [{ message: 'z', range: { start: { line: 1, character: 2 } } }]); const back = new dc.DiagCache(JSON.parse(JSON.stringify(cache))); assert.deepStrictEqual(back.get('k1'), [{ message: 'z', range: { start: { line: 1, character: 2 } } }]); }); check('a saved entry that is not well formed is dropped, not trusted', () => { const back = new dc.DiagCache({ bad: { key: 'bad' }, good: [] }); assert.strictEqual(back.get('bad'), null); assert.deepStrictEqual(back.get('good'), []); }); fs.rmSync(root, { recursive: true, force: true }); console.log(passed + ' passed, ' + failed + ' failed'); process.exit(failed ? 1 : 0);