From 1eb0f5371ae8cee90b97f586a99505cfa5913504 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sat, 8 Feb 2020 17:25:53 -0800 Subject: LSP: fix validate_client_config - `cmd_env` is a table not a function. - tests: Set $NVIM_LOG_FILE for fake LSP server. --- runtime/lua/vim/lsp.lua | 2 +- test/functional/fixtures/fake-lsp-server.lua | 438 +++++++++++++++++++++++ test/functional/fixtures/lsp-test-rpc-server.lua | 438 ----------------------- test/functional/plugin/lsp_spec.lua | 13 +- 4 files changed, 447 insertions(+), 444 deletions(-) create mode 100644 test/functional/fixtures/fake-lsp-server.lua delete mode 100644 test/functional/fixtures/lsp-test-rpc-server.lua diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index e5b6653346..94f0d62d8d 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -154,7 +154,7 @@ local function validate_client_config(config) callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; - cmd_env = { config.cmd_env, "f", true }; + cmd_env = { config.cmd_env, "t", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; diff --git a/test/functional/fixtures/fake-lsp-server.lua b/test/functional/fixtures/fake-lsp-server.lua new file mode 100644 index 0000000000..44117bea30 --- /dev/null +++ b/test/functional/fixtures/fake-lsp-server.lua @@ -0,0 +1,438 @@ +local protocol = require 'vim.lsp.protocol' + + +local function message_parts(sep, ...) + local parts = {} + for i = 1, select("#", ...) do + local arg = select(i, ...) + if arg ~= nil then + table.insert(parts, arg) + end + end + return table.concat(parts, sep) +end + +-- Assert utility methods + +local function assert_eq(a, b, ...) + if not vim.deep_equal(a, b) then + error(message_parts(": ", + ..., "assert_eq failed", + string.format("left == %q, right == %q", vim.inspect(a), vim.inspect(b)) + )) + end +end + +local function format_message_with_content_length(encoded_message) + return table.concat { + 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n'; + encoded_message; + } +end + +local function read_message() + local line = io.read("*l") + local length = line:lower():match("content%-length:%s*(%d+)") + return vim.fn.json_decode(io.read(2 + length):sub(2)) +end + +local function send(payload) + io.stdout:write(format_message_with_content_length(vim.fn.json_encode(payload))) +end + +local function respond(id, err, result) + assert(type(id) == 'number', "id must be a number") + send { jsonrpc = "2.0"; id = id, error = err, result = result } +end + +local function notify(method, params) + assert(type(method) == 'string', "method must be a string") + send { method = method, params = params or {} } +end + +local function expect_notification(method, params, ...) + local message = read_message() + assert_eq(method, message.method, + ..., "expect_notification", "method") + assert_eq(params, message.params, + ..., "expect_notification", method, "params") + assert_eq({jsonrpc = "2.0"; method=method, params=params}, message, + ..., "expect_notification", "message") +end + +local function expect_request(method, callback, ...) + local req = read_message() + assert_eq(method, req.method, + ..., "expect_request", "method") + local err, result = callback(req.params) + respond(req.id, err, result) +end + +io.stderr:setvbuf("no") + +local function skeleton(config) + local on_init = assert(config.on_init) + local body = assert(config.body) + expect_request("initialize", function(params) + return nil, on_init(params) + end) + expect_notification("initialized", {}) + body() + expect_request("shutdown", function() + return nil, {} + end) + expect_notification("exit", nil) +end + +-- The actual tests. + +local tests = {} + +function tests.basic_init() + skeleton { + on_init = function(_params) + return { capabilities = {} } + end; + body = function() + notify('test') + end; + } +end + +function tests.basic_check_capabilities() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + end; + } +end + +function tests.basic_finish() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n") .. '\n'; + uri = "file://"; + version = 0; + }; + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open_and_change() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n") .. '\n'; + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; + } + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open_and_change_noeol() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n"); + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { text = table.concat({"testing"; "boop"}, "\n"); }; + } + }) + expect_notification("finish") + notify('finish') + end; + } +end +function tests.basic_check_buffer_open_and_change_multi() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n") .. '\n'; + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { text = table.concat({"testing"; "321"}, "\n") .. '\n'; }; + } + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 4; + }; + contentChanges = { + { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; + } + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open_and_change_multi_and_close() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Full; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n") .. '\n'; + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { text = table.concat({"testing"; "321"}, "\n") .. '\n'; }; + } + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 4; + }; + contentChanges = { + { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; + } + }) + expect_notification('textDocument/didClose', { + textDocument = { + uri = "file://"; + }; + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open_and_change_incremental() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Incremental; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n") .. '\n'; + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { + range = { + start = { line = 1; character = 0; }; + ["end"] = { line = 2; character = 0; }; + }; + rangeLength = 4; + text = "boop\n"; + }; + } + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.basic_check_buffer_open_and_change_incremental_editting() + skeleton { + on_init = function(params) + local expected_capabilities = protocol.make_client_capabilities() + assert_eq(params.capabilities, expected_capabilities) + return { + capabilities = { + textDocumentSync = protocol.TextDocumentSyncKind.Incremental; + } + } + end; + body = function() + notify('start') + expect_notification('textDocument/didOpen', { + textDocument = { + languageId = ""; + text = table.concat({"testing"; "123"}, "\n"); + uri = "file://"; + version = 0; + }; + }) + expect_notification('textDocument/didChange', { + textDocument = { + uri = "file://"; + version = 3; + }; + contentChanges = { + { + range = { + start = { line = 0; character = 0; }; + ["end"] = { line = 1; character = 0; }; + }; + rangeLength = 4; + text = "testing\n\n"; + }; + } + }) + expect_notification("finish") + notify('finish') + end; + } +end + +function tests.invalid_header() + io.stdout:write("Content-length: \r\n") +end + +-- Tests will be indexed by TEST_NAME + +local kill_timer = vim.loop.new_timer() +kill_timer:start(_G.TIMEOUT or 1e3, 0, function() + kill_timer:stop() + kill_timer:close() + io.stderr:write("TIMEOUT") + os.exit(100) +end) + +local test_name = _G.TEST_NAME -- lualint workaround +assert(type(test_name) == 'string', 'TEST_NAME must be specified.') +local status, err = pcall(assert(tests[test_name], "Test not found")) +kill_timer:stop() +kill_timer:close() +if not status then + io.stderr:write(err) + os.exit(101) +end +os.exit(0) diff --git a/test/functional/fixtures/lsp-test-rpc-server.lua b/test/functional/fixtures/lsp-test-rpc-server.lua deleted file mode 100644 index 44117bea30..0000000000 --- a/test/functional/fixtures/lsp-test-rpc-server.lua +++ /dev/null @@ -1,438 +0,0 @@ -local protocol = require 'vim.lsp.protocol' - - -local function message_parts(sep, ...) - local parts = {} - for i = 1, select("#", ...) do - local arg = select(i, ...) - if arg ~= nil then - table.insert(parts, arg) - end - end - return table.concat(parts, sep) -end - --- Assert utility methods - -local function assert_eq(a, b, ...) - if not vim.deep_equal(a, b) then - error(message_parts(": ", - ..., "assert_eq failed", - string.format("left == %q, right == %q", vim.inspect(a), vim.inspect(b)) - )) - end -end - -local function format_message_with_content_length(encoded_message) - return table.concat { - 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n'; - encoded_message; - } -end - -local function read_message() - local line = io.read("*l") - local length = line:lower():match("content%-length:%s*(%d+)") - return vim.fn.json_decode(io.read(2 + length):sub(2)) -end - -local function send(payload) - io.stdout:write(format_message_with_content_length(vim.fn.json_encode(payload))) -end - -local function respond(id, err, result) - assert(type(id) == 'number', "id must be a number") - send { jsonrpc = "2.0"; id = id, error = err, result = result } -end - -local function notify(method, params) - assert(type(method) == 'string', "method must be a string") - send { method = method, params = params or {} } -end - -local function expect_notification(method, params, ...) - local message = read_message() - assert_eq(method, message.method, - ..., "expect_notification", "method") - assert_eq(params, message.params, - ..., "expect_notification", method, "params") - assert_eq({jsonrpc = "2.0"; method=method, params=params}, message, - ..., "expect_notification", "message") -end - -local function expect_request(method, callback, ...) - local req = read_message() - assert_eq(method, req.method, - ..., "expect_request", "method") - local err, result = callback(req.params) - respond(req.id, err, result) -end - -io.stderr:setvbuf("no") - -local function skeleton(config) - local on_init = assert(config.on_init) - local body = assert(config.body) - expect_request("initialize", function(params) - return nil, on_init(params) - end) - expect_notification("initialized", {}) - body() - expect_request("shutdown", function() - return nil, {} - end) - expect_notification("exit", nil) -end - --- The actual tests. - -local tests = {} - -function tests.basic_init() - skeleton { - on_init = function(_params) - return { capabilities = {} } - end; - body = function() - notify('test') - end; - } -end - -function tests.basic_check_capabilities() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - end; - } -end - -function tests.basic_finish() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n") .. '\n'; - uri = "file://"; - version = 0; - }; - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open_and_change() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n") .. '\n'; - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; - } - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open_and_change_noeol() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n"); - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { text = table.concat({"testing"; "boop"}, "\n"); }; - } - }) - expect_notification("finish") - notify('finish') - end; - } -end -function tests.basic_check_buffer_open_and_change_multi() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n") .. '\n'; - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { text = table.concat({"testing"; "321"}, "\n") .. '\n'; }; - } - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 4; - }; - contentChanges = { - { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; - } - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open_and_change_multi_and_close() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Full; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n") .. '\n'; - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { text = table.concat({"testing"; "321"}, "\n") .. '\n'; }; - } - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 4; - }; - contentChanges = { - { text = table.concat({"testing"; "boop"}, "\n") .. '\n'; }; - } - }) - expect_notification('textDocument/didClose', { - textDocument = { - uri = "file://"; - }; - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open_and_change_incremental() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Incremental; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n") .. '\n'; - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { - range = { - start = { line = 1; character = 0; }; - ["end"] = { line = 2; character = 0; }; - }; - rangeLength = 4; - text = "boop\n"; - }; - } - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.basic_check_buffer_open_and_change_incremental_editting() - skeleton { - on_init = function(params) - local expected_capabilities = protocol.make_client_capabilities() - assert_eq(params.capabilities, expected_capabilities) - return { - capabilities = { - textDocumentSync = protocol.TextDocumentSyncKind.Incremental; - } - } - end; - body = function() - notify('start') - expect_notification('textDocument/didOpen', { - textDocument = { - languageId = ""; - text = table.concat({"testing"; "123"}, "\n"); - uri = "file://"; - version = 0; - }; - }) - expect_notification('textDocument/didChange', { - textDocument = { - uri = "file://"; - version = 3; - }; - contentChanges = { - { - range = { - start = { line = 0; character = 0; }; - ["end"] = { line = 1; character = 0; }; - }; - rangeLength = 4; - text = "testing\n\n"; - }; - } - }) - expect_notification("finish") - notify('finish') - end; - } -end - -function tests.invalid_header() - io.stdout:write("Content-length: \r\n") -end - --- Tests will be indexed by TEST_NAME - -local kill_timer = vim.loop.new_timer() -kill_timer:start(_G.TIMEOUT or 1e3, 0, function() - kill_timer:stop() - kill_timer:close() - io.stderr:write("TIMEOUT") - os.exit(100) -end) - -local test_name = _G.TEST_NAME -- lualint workaround -assert(type(test_name) == 'string', 'TEST_NAME must be specified.') -local status, err = pcall(assert(tests[test_name], "Test not found")) -kill_timer:stop() -kill_timer:close() -if not status then - io.stderr:write(err) - os.exit(101) -end -os.exit(0) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index cab1fb0d79..c384fdedf3 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -16,16 +16,16 @@ local run, stop = helpers.run, helpers.stop if helpers.pending_win32(pending) then return end -local fake_lsp_server_file = "test/functional/fixtures/lsp-test-rpc-server.lua" -if iswin() then - fake_lsp_server_file = fake_lsp_server_file:gsub("/", "\\") -end +local fake_lsp_server_file = 'test/functional/fixtures/fake-lsp-server.lua' local function fake_lsp_server_setup(test_name, timeout_ms) exec_lua([=[ lsp = require('vim.lsp') local test_name, fixture_filename, timeout = ... TEST_RPC_CLIENT_ID = lsp.start_client { + cmd_env = { + NVIM_LOG_FILE = 'Xtest-fake-lsp-server.log' + }; cmd = { vim.v.progpath, '-Es', '-u', 'NONE', '--headless', "-c", string.format("lua TEST_NAME = %q", test_name), @@ -117,6 +117,9 @@ describe('LSP', function() local test_name, fixture_filename = ... function test__start_client() return lsp.start_client { + cmd_env = { + NVIM_LOG_FILE = 'Xtest-fake-lsp-server.log' + }; cmd = { vim.v.progpath, '-Es', '-u', 'NONE', '--headless', "-c", string.format("lua TEST_NAME = %q", test_name), @@ -217,7 +220,7 @@ describe('LSP', function() client.stop() end; on_exit = function(code, signal) - eq(101, code, "exit code") + eq(101, code, "exit code") -- See fake-lsp-server.lua eq(0, signal, "exit signal") end; on_callback = function(...) -- cgit