aboutsummaryrefslogtreecommitdiff
path: root/tests/drivers
diff options
context:
space:
mode:
Diffstat (limited to 'tests/drivers')
-rw-r--r--tests/drivers/codeactions_client_test.js2
-rw-r--r--tests/drivers/hint_fixes_test.js66
-rw-r--r--tests/drivers/toolchain_lsp_contract_test.js4
3 files changed, 69 insertions, 3 deletions
diff --git a/tests/drivers/codeactions_client_test.js b/tests/drivers/codeactions_client_test.js
index f1936e7..6f956b9 100644
--- a/tests/drivers/codeactions_client_test.js
+++ b/tests/drivers/codeactions_client_test.js
@@ -9,7 +9,7 @@ class CodeAction { constructor(title, kind) { this.title = title; this.kind = ki
const vscode = { Range, WorkspaceEdit, CodeAction, CodeActionKind: { QuickFix: 'quickfix' }, Uri: { parse: s => s } };
const uri = 'file:///test.zc', doc = { uri: { toString: () => uri }, version: 3 };
const edit = { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } }, newText: 'include rt;\n' };
-const source = { title: 'include rt;', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } };
+const source = { title: 'include rt', kind: 'quickfix', isPreferred: true, edit: { documentChanges: [{ textDocument: { uri, version: 3 }, edits: [edit] }] } };
const actions = convertCodeActions(vscode, [source], doc);
assert.strictEqual(actions.length, 1);
assert.strictEqual(actions[0].kind, 'quickfix');
diff --git a/tests/drivers/hint_fixes_test.js b/tests/drivers/hint_fixes_test.js
new file mode 100644
index 0000000..9513a85
--- /dev/null
+++ b/tests/drivers/hint_fixes_test.js
@@ -0,0 +1,66 @@
+// quick fixes for hints and for library names used without their module: each fix is the
+// edit the hint describes, offered only where the change keeps the program's meaning.
+// usage: node tests/drivers/hint_fixes_test.js path/to/zclsp
+// interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline
+const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1;
+const cp = require('child_process');
+const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); };
+let ok = 0, bad = 0;
+function check(cond, label, extra) {
+ if (cond) { ok++; console.log('ok ' + label); }
+ else { bad++; console.log('FAIL ' + label + (extra === undefined ? '' : ': ' + JSON.stringify(extra).slice(0, 400))); }
+}
+// the actions offered at the first occurrence of needle (plus offset) in text
+function actions(text, needle, offset = 0) {
+ const at = text.indexOf(needle) + offset, before = text.slice(0, at);
+ const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 };
+ const uri = 'file:///hint-fixes.zc';
+ const messages = [{ id: 1, method: 'initialize', params: {} },
+ { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } },
+ { id: 2, method: 'textDocument/codeAction', params: { textDocument: { uri }, range: { start: position, end: position }, context: { diagnostics: [] } } },
+ { method: 'exit' }];
+ const r = cp.spawnSync(process.argv[2], [], { input: Buffer.concat(messages.map(m => frame({ jsonrpc: '2.0', ...m }))), timeout: 30000 * TIMEOUT_SCALE });
+ const out = r.stdout.toString(), i = out.indexOf('{"jsonrpc":"2.0","id":2');
+ return i < 0 ? null : JSON.parse(out.slice(i, out.indexOf('Content-Length', i) < 0 ? undefined : out.indexOf('Content-Length', i))).result;
+}
+// apply the first action's edits to text
+function apply(text, action) {
+ const lines = text.split('\n');
+ const offset = p => lines.slice(0, p.line).reduce((n, l) => n + l.length + 1, 0) + p.character;
+ const edits = action.edit.documentChanges[0].edits.map(e => [offset(e.range.start), offset(e.range.end), e.newText]).sort((a, b) => b[0] - a[0]);
+ for (const [s, e, t] of edits) text = text.slice(0, s) + t + text.slice(e);
+ return text;
+}
+function fixes(label, text, needle, offset, expected, title) {
+ const result = actions(text, needle, offset);
+ const first = result && result[0];
+ check(first && first.title === title && first.isPreferred === true && apply(text, first) === expected, label, result);
+}
+function none(label, text, needle, offset) {
+ const result = actions(text, needle, offset);
+ check(Array.isArray(result) && result.length === 0, label, result);
+}
+const head = 'include rt;\nfn main() {\n';
+fixes('an unused parameter is renamed to _', 'include rt;\nint g(int a, int b) { return a; }\nfn main() { return g(1, 2); }\n', 'int b', 4,
+ 'include rt;\nint g(int a, int _) { return a; }\nfn main() { return g(1, 2); }\n', 'rename `b` to _');
+fixes('a local that is never read is renamed to _', head + '\tlet unused = rt.get_c();\n\treturn 0;\n}\n', 'unused', 0,
+ head + '\tlet _ = rt.get_c();\n\treturn 0;\n}\n', 'rename `unused` to _');
+none('a local written later is not renamed', head + '\tlet later = 1;\n\tlater = 2;\n\treturn 0;\n}\n', 'later', 0);
+fixes('a typed constant local becomes const', head + '\tint x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'x = 5', 0,
+ head + '\tconst int x = 5;\n\trt.put_n(x);\n\treturn 0;\n}\n', 'make `x` a const');
+fixes('an untyped constant local becomes const int', head + '\tlet y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'y = 7', 0,
+ head + '\tconst int y = 7;\n\trt.put_n(y);\n\treturn 0;\n}\n', 'make `y` a const');
+none('one of several declarators is left alone', head + '\tint x = 5, z = rt.get_c();\n\trt.put_n(x + z);\n\treturn 0;\n}\n', 'x = 5', 0);
+fixes('a power-of-two multiply becomes a parenthesized shift', head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n(q[0] * 16 + y);\n\treturn 0;\n}\n', '* 16', 0,
+ head + '\tlet y = rt.get_c();\n\tlet q[4];\n\tq[0] = y;\n\trt.put_n((q[0] << 4) + y);\n\treturn 0;\n}\n', 'write << 4');
+none('a multiply bound to a division on its left is left alone', head + '\tlet y = rt.get_c();\n\trt.put_n(y / 2 * 16);\n\treturn 0;\n}\n', '* 16', 0);
+fixes('a small heap buffer becomes a stack buffer', head + '\tlet p = rt.new(8);\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'p = rt', 0,
+ head + '\tlet p[8];\n\tp[0] = rt.get_c();\n\trt.put_n(p[0]);\n\treturn 0;\n}\n', 'use a stack buffer');
+fixes('a runtime name without its module is imported and qualified', 'fn main() {\n\tput_c(65);\n\treturn 0;\n}\n', 'put_c', 0,
+ 'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'include rt and use rt.put_c');
+fixes('a compiler name is imported from run', 'include rt;\nfn main() {\n\trt.put_n(evals("return 1;"));\n\treturn 0;\n}\n', 'evals', 0,
+ 'include rt;\ninclude run;\nfn main() {\n\trt.put_n(run.evals("return 1;"));\n\treturn 0;\n}\n', 'include run and use run.evals');
+fixes('an imported module only needs the qualifier', 'include rt;\nfn main() {\n\tput_c(65);\n\treturn 0;\n}\n', 'put_c', 0,
+ 'include rt;\nfn main() {\n\trt.put_c(65);\n\treturn 0;\n}\n', 'use rt.put_c');
+console.log(ok + ' hint fix checks passed, ' + bad + ' failed');
+process.exit(bad ? 1 : 0);
diff --git a/tests/drivers/toolchain_lsp_contract_test.js b/tests/drivers/toolchain_lsp_contract_test.js
index 2872282..59d5a4c 100644
--- a/tests/drivers/toolchain_lsp_contract_test.js
+++ b/tests/drivers/toolchain_lsp_contract_test.js
@@ -284,12 +284,12 @@ test('inference', 'typed fields do not constrain the containing parameter to a s
test('quickfix', 'an entire-line request finds the compiler error inside the range', () => {
const source = 'fn main() { rt.put_n(7); }';
const r = run(source, [{ needle: 'fn', method: 'textDocument/codeAction', params: { range: { start: { line: 0, character: 0 }, end: { line: 0, character: source.length } }, context: { diagnostics: [{ range: { start: { line: 99, character: 99 }, end: { line: 99, character: 99 } }, message: 'stale' }] } } }]);
- assert.ok(r.result(2)?.some(a => a.title === 'include rt;'), JSON.stringify(r.result(2)));
+ assert.ok(r.result(2)?.some(a => a.title === 'include rt'), JSON.stringify(r.result(2)));
});
test('quickfix', 'an owner-name cursor can import a qualified module', () => {
const source = 'fn main() { rt.put_n(7); }', start = { line: 0, character: source.indexOf('rt.') };
const r = run(source, [{ needle: 'rt.', method: 'textDocument/codeAction', params: { range: { start, end: start }, context: { diagnostics: [] } } }]);
- assert.ok(r.result(2)?.some(a => a.title === 'include rt;'));
+ assert.ok(r.result(2)?.some(a => a.title === 'include rt'));
});
test('modulehover', 'source library overview is available without a built-in module entry', () => {
const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'zc-module-hover-'));