diff options
Diffstat (limited to 'test/functional/api')
-rw-r--r-- | test/functional/api/buffer_spec.lua | 52 | ||||
-rw-r--r-- | test/functional/api/keymap_spec.lua | 310 | ||||
-rw-r--r-- | test/functional/api/server_notifications_spec.lua | 8 | ||||
-rw-r--r-- | test/functional/api/server_requests_spec.lua | 116 | ||||
-rw-r--r-- | test/functional/api/tabpage_spec.lua | 7 | ||||
-rw-r--r-- | test/functional/api/version_spec.lua | 134 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 283 | ||||
-rw-r--r-- | test/functional/api/window_spec.lua | 14 |
8 files changed, 871 insertions, 53 deletions
diff --git a/test/functional/api/buffer_spec.lua b/test/functional/api/buffer_spec.lua index 3d3a2bb046..823c134ebd 100644 --- a/test/functional/api/buffer_spec.lua +++ b/test/functional/api/buffer_spec.lua @@ -2,8 +2,15 @@ local helpers = require('test.functional.helpers')(after_each) local clear, nvim, buffer = helpers.clear, helpers.nvim, helpers.buffer local curbuf, curwin, eq = helpers.curbuf, helpers.curwin, helpers.eq local curbufmeths, ok = helpers.curbufmeths, helpers.ok -local funcs, request = helpers.funcs, helpers.request +local funcs = helpers.funcs +local request = helpers.request +local exc_exec = helpers.exc_exec +local feed_command = helpers.feed_command +local insert = helpers.insert local NIL = helpers.NIL +local meth_pcall = helpers.meth_pcall +local command = helpers.command +local bufmeths = helpers.bufmeths describe('api/buf', function() before_each(clear) @@ -115,6 +122,15 @@ describe('api/buf', function() local get_lines, set_lines = curbufmeths.get_lines, curbufmeths.set_lines local line_count = curbufmeths.line_count + it('fails correctly when input is not valid', function() + eq(1, curbufmeths.get_number()) + local err, emsg = pcall(bufmeths.set_lines, 1, 1, 2, false, {'b\na'}) + eq(false, err) + local exp_emsg = 'String cannot contain newlines' + -- Expected {filename}:{lnum}: {exp_emsg} + eq(': ' .. exp_emsg, emsg:sub(-#exp_emsg - 2)) + end) + it('has correct line_count when inserting and deleting', function() eq(1, line_count()) set_lines(-1, -1, true, {'line'}) @@ -239,6 +255,22 @@ describe('api/buf', function() eq({'e', 'a', 'b', 'c', 'd'}, get_lines(0, -1, true)) end) + it("set_line on alternate buffer does not access invalid line (E315)", function() + feed_command('set hidden') + insert('Initial file') + command('enew') + insert([[ + More + Lines + Than + In + The + Other + Buffer]]) + feed_command('$') + local retval = exc_exec("call nvim_buf_set_lines(1, 0, 1, v:false, ['test'])") + eq(0, retval) + end) end) describe('{get,set,del}_var', function() @@ -249,6 +281,24 @@ describe('api/buf', function() eq(1, funcs.exists('b:lua')) curbufmeths.del_var('lua') eq(0, funcs.exists('b:lua')) + eq({false, 'Key does not exist: lua'}, meth_pcall(curbufmeths.del_var, 'lua')) + curbufmeths.set_var('lua', 1) + command('lockvar b:lua') + eq({false, 'Key is locked: lua'}, meth_pcall(curbufmeths.del_var, 'lua')) + eq({false, 'Key is locked: lua'}, meth_pcall(curbufmeths.set_var, 'lua', 1)) + eq({false, 'Key is read-only: changedtick'}, + meth_pcall(curbufmeths.del_var, 'changedtick')) + eq({false, 'Key is read-only: changedtick'}, + meth_pcall(curbufmeths.set_var, 'changedtick', 1)) + end) + end) + + describe('get_changedtick', function() + it('works', function() + eq(2, curbufmeths.get_changedtick()) + curbufmeths.set_lines(0, 1, false, {'abc\0', '\0def', 'ghi'}) + eq(3, curbufmeths.get_changedtick()) + eq(3, curbufmeths.get_var('changedtick')) end) it('buffer_set_var returns the old value', function() diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua new file mode 100644 index 0000000000..3a10f9c60f --- /dev/null +++ b/test/functional/api/keymap_spec.lua @@ -0,0 +1,310 @@ +local helpers = require('test.functional.helpers')(after_each) +local global_helpers = require('test.helpers') + +local clear = helpers.clear +local command = helpers.command +local curbufmeths = helpers.curbufmeths +local eq = helpers.eq +local funcs = helpers.funcs +local meths = helpers.meths +local source = helpers.source + +local shallowcopy = global_helpers.shallowcopy + +describe('get_keymap', function() + before_each(clear) + + -- Basic mapping and table to be used to describe results + local foo_bar_string = 'nnoremap foo bar' + local foo_bar_map_table = { + lhs='foo', + silent=0, + rhs='bar', + expr=0, + sid=0, + buffer=0, + nowait=0, + mode='n', + noremap=1, + } + + it('returns empty list when no map', function() + eq({}, meths.get_keymap('n')) + end) + + it('returns list of all applicable mappings', function() + command(foo_bar_string) + -- Only one mapping available + -- Should be the same as the dictionary we supplied earlier + -- and the dictionary you would get from maparg + -- since this is a global map, and not script local + eq({foo_bar_map_table}, meths.get_keymap('n')) + eq({funcs.maparg('foo', 'n', false, true)}, + meths.get_keymap('n') + ) + + -- Add another mapping + command('nnoremap foo_longer bar_longer') + local foolong_bar_map_table = shallowcopy(foo_bar_map_table) + foolong_bar_map_table['lhs'] = 'foo_longer' + foolong_bar_map_table['rhs'] = 'bar_longer' + + eq({foolong_bar_map_table, foo_bar_map_table}, + meths.get_keymap('n') + ) + + -- Remove a mapping + command('unmap foo_longer') + eq({foo_bar_map_table}, + meths.get_keymap('n') + ) + end) + + it('works for other modes', function() + -- Add two mappings, one in insert and one normal + -- We'll only check the insert mode one + command('nnoremap not_going to_check') + + command('inoremap foo bar') + -- The table will be the same except for the mode + local insert_table = shallowcopy(foo_bar_map_table) + insert_table['mode'] = 'i' + + eq({insert_table}, meths.get_keymap('i')) + end) + + it('considers scope', function() + -- change the map slightly + command('nnoremap foo_longer bar_longer') + local foolong_bar_map_table = shallowcopy(foo_bar_map_table) + foolong_bar_map_table['lhs'] = 'foo_longer' + foolong_bar_map_table['rhs'] = 'bar_longer' + + local buffer_table = shallowcopy(foo_bar_map_table) + buffer_table['buffer'] = 1 + + command('nnoremap <buffer> foo bar') + + -- The buffer mapping should not show up + eq({foolong_bar_map_table}, meths.get_keymap('n')) + eq({buffer_table}, curbufmeths.get_keymap('n')) + end) + + it('considers scope for overlapping maps', function() + command('nnoremap foo bar') + + local buffer_table = shallowcopy(foo_bar_map_table) + buffer_table['buffer'] = 1 + + command('nnoremap <buffer> foo bar') + + eq({foo_bar_map_table}, meths.get_keymap('n')) + eq({buffer_table}, curbufmeths.get_keymap('n')) + end) + + it('can retrieve mapping for different buffers', function() + local original_buffer = curbufmeths.get_number() + -- Place something in each of the buffers to make sure they stick around + -- and set hidden so we can leave them + command('set hidden') + command('new') + command('normal! ihello 2') + command('new') + command('normal! ihello 3') + + local final_buffer = curbufmeths.get_number() + + command('nnoremap <buffer> foo bar') + -- Final buffer will have buffer mappings + local buffer_table = shallowcopy(foo_bar_map_table) + buffer_table['buffer'] = final_buffer + eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n')) + eq({buffer_table}, meths.buf_get_keymap(0, 'n')) + + command('buffer ' .. original_buffer) + eq(original_buffer, curbufmeths.get_number()) + -- Original buffer won't have any mappings + eq({}, meths.get_keymap('n')) + eq({}, curbufmeths.get_keymap('n')) + eq({buffer_table}, meths.buf_get_keymap(final_buffer, 'n')) + end) + + -- Test toggle switches for basic options + -- @param option The key represented in the `maparg()` result dict + local function global_and_buffer_test(map, + option, + option_token, + global_on_result, + buffer_on_result, + global_off_result, + buffer_off_result, + new_windows) + + local function make_new_windows(number_of_windows) + if new_windows == nil then + return nil + end + + for _=1,number_of_windows do + command('new') + end + end + + local mode = string.sub(map, 1,1) + -- Don't run this for the <buffer> mapping, since it doesn't make sense + if option_token ~= '<buffer>' then + it(string.format( 'returns %d for the key "%s" when %s is used globally with %s (%s)', + global_on_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' ' .. option_token .. ' foo bar') + local result = meths.get_keymap(mode)[1][option] + eq(global_on_result, result) + end) + end + + it(string.format('returns %d for the key "%s" when %s is used for buffers with %s (%s)', + buffer_on_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' <buffer> ' .. option_token .. ' foo bar') + local result = curbufmeths.get_keymap(mode)[1][option] + eq(buffer_on_result, result) + end) + + -- Don't run these for the <buffer> mapping, since it doesn't make sense + if option_token ~= '<buffer>' then + it(string.format('returns %d for the key "%s" when %s is not used globally with %s (%s)', + global_off_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' baz bat') + local result = meths.get_keymap(mode)[1][option] + eq(global_off_result, result) + end) + + it(string.format('returns %d for the key "%s" when %s is not used for buffers with %s (%s)', + buffer_off_result, option, option_token, map, mode), function() + make_new_windows(new_windows) + command(map .. ' <buffer> foo bar') + + local result = curbufmeths.get_keymap(mode)[1][option] + eq(buffer_off_result, result) + end) + end + end + + -- Standard modes and returns the same values in the dictionary as maparg() + local mode_list = {'nnoremap', 'nmap', 'imap', 'inoremap', 'cnoremap'} + for mode in pairs(mode_list) do + global_and_buffer_test(mode_list[mode], 'silent', '<silent>', 1, 1, 0, 0) + global_and_buffer_test(mode_list[mode], 'nowait', '<nowait>', 1, 1, 0, 0) + global_and_buffer_test(mode_list[mode], 'expr', '<expr>', 1, 1, 0, 0) + end + + -- noremap will now be 2 if script was used, which is not the same as maparg() + global_and_buffer_test('nmap', 'noremap', '<script>', 2, 2, 0, 0) + global_and_buffer_test('nnoremap', 'noremap', '<script>', 2, 2, 1, 1) + + -- buffer will return the buffer ID, which is not the same as maparg() + -- Three of these tests won't run + global_and_buffer_test('nnoremap', 'buffer', '<buffer>', nil, 3, nil, nil, 2) + + it('returns script numbers for global maps', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap fizz :call <SID>maparg_test_function()<CR> + ]]) + local sid_result = meths.get_keymap('n')[1]['sid'] + eq(1, sid_result) + eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {})) + end) + + it('returns script numbers for buffer maps', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap <buffer> fizz :call <SID>maparg_test_function()<CR> + ]]) + local sid_result = curbufmeths.get_keymap('n')[1]['sid'] + eq(1, sid_result) + eq('testing', meths.call_function('<SNR>' .. sid_result .. '_maparg_test_function', {})) + end) + + it('works with <F12> and others', function() + command('nnoremap <F12> :let g:maparg_test_var = 1<CR>') + eq('<F12>', meths.get_keymap('n')[1]['lhs']) + eq(':let g:maparg_test_var = 1<CR>', meths.get_keymap('n')[1]['rhs']) + end) + + it('works correctly despite various &cpo settings', function() + local cpo_table = { + silent=0, + expr=0, + sid=0, + buffer=0, + nowait=0, + noremap=1, + } + local function cpomap(lhs, rhs, mode) + local ret = shallowcopy(cpo_table) + ret.lhs = lhs + ret.rhs = rhs + ret.mode = mode + return ret + end + + command('set cpo+=B') + command('nnoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\') + command('nnoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\') + + command('set cpo+=B') + command('xnoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\') + command('xnoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\') + + command('set cpo-=B') + command('snoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\') + command('snoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\') + + command('set cpo-=B') + command('onoremap \\<C-a><C-a><LT>C-a>\\ \\<C-b><C-b><LT>C-b>\\') + command('onoremap <special> \\<C-c><C-c><LT>C-c>\\ \\<C-d><C-d><LT>C-d>\\') + + for _, cmd in ipairs({ + 'set cpo-=B', + 'set cpo+=B', + }) do + command(cmd) + eq({cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'n'), + cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'n')}, + meths.get_keymap('n')) + eq({cpomap('\\<C-C><C-C><lt>C-c>\\', '\\<C-D><C-D><lt>C-d>\\', 'x'), + cpomap('\\<C-A><C-A><lt>C-a>\\', '\\<C-B><C-B><lt>C-b>\\', 'x')}, + meths.get_keymap('x')) + eq({cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 's'), + cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 's')}, + meths.get_keymap('s')) + eq({cpomap('<lt>C-c><C-C><lt>C-c> ', '<lt>C-d><C-D><lt>C-d>', 'o'), + cpomap('<lt>C-a><C-A><lt>C-a> ', '<lt>C-b><C-B><lt>C-b>', 'o')}, + meths.get_keymap('o')) + end + end) + + it('always uses space for space and bar for bar', function() + local space_table = { + lhs='| |', + rhs='| |', + mode='n', + silent=0, + expr=0, + sid=0, + buffer=0, + nowait=0, + noremap=1, + } + command('nnoremap \\|<Char-0x20><Char-32><Space><Bar> \\|<Char-0x20><Char-32><Space> <Bar>') + eq({space_table}, meths.get_keymap('n')) + end) +end) diff --git a/test/functional/api/server_notifications_spec.lua b/test/functional/api/server_notifications_spec.lua index 78639d7ed7..9d7cfb9b78 100644 --- a/test/functional/api/server_notifications_spec.lua +++ b/test/functional/api/server_notifications_spec.lua @@ -1,6 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) -local eq, clear, eval, execute, nvim, next_message = - helpers.eq, helpers.clear, helpers.eval, helpers.execute, helpers.nvim, +local eq, clear, eval, command, nvim, next_message = + helpers.eq, helpers.clear, helpers.eval, helpers.command, helpers.nvim, helpers.next_message local meths = helpers.meths @@ -16,8 +16,8 @@ describe('notify', function() it('sends the notification/args to the corresponding channel', function() eval('rpcnotify('..channel..', "test-event", 1, 2, 3)') eq({'notification', 'test-event', {1, 2, 3}}, next_message()) - execute('au FileType lua call rpcnotify('..channel..', "lua!")') - execute('set filetype=lua') + command('au FileType lua call rpcnotify('..channel..', "lua!")') + command('set filetype=lua') eq({'notification', 'lua!', {}}, next_message()) end) end) diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index 3245e1b52d..9f245d913b 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -1,14 +1,16 @@ --- Tests for some server->client RPC scenarios. Note that unlike with --- `rpcnotify`, to evaluate `rpcrequest` calls we need the client event loop to --- be running. +-- Test server -> client RPC scenarios. Note: unlike `rpcnotify`, to evaluate +-- `rpcrequest` calls we need the client event loop to be running. local helpers = require('test.functional.helpers')(after_each) +local Paths = require('test.config.paths') + local clear, nvim, eval = helpers.clear, helpers.nvim, helpers.eval local eq, neq, run, stop = helpers.eq, helpers.neq, helpers.run, helpers.stop local nvim_prog, command, funcs = helpers.nvim_prog, helpers.command, helpers.funcs local source, next_message = helpers.source, helpers.next_message +local ok = helpers.ok local meths = helpers.meths - -if helpers.pending_win32(pending) then return end +local spawn, nvim_argv = helpers.spawn, helpers.nvim_argv +local set_session = helpers.set_session describe('server -> client', function() local cid @@ -18,6 +20,22 @@ describe('server -> client', function() cid = nvim('get_api_info')[1] end) + it('handles unexpected closed stream while preparing RPC response', function() + source([[ + let g:_nvim_args = [v:progpath, '--embed', '-n', '-u', 'NONE', '-i', 'NONE', ] + let ch1 = jobstart(g:_nvim_args, {'rpc': v:true}) + let child1_ch = rpcrequest(ch1, "nvim_get_api_info")[0] + call rpcnotify(ch1, 'nvim_eval', 'rpcrequest('.child1_ch.', "nvim_get_api_info")') + + let ch2 = jobstart(g:_nvim_args, {'rpc': v:true}) + let child2_ch = rpcrequest(ch2, "nvim_get_api_info")[0] + call rpcnotify(ch2, 'nvim_eval', 'rpcrequest('.child2_ch.', "nvim_get_api_info")') + + call jobstop(ch1) + ]]) + eq(2, eval("1+1")) -- Still alive? + end) + describe('simple call', function() it('works', function() local function on_setup() @@ -139,7 +157,7 @@ describe('server -> client', function() end) end) - describe('when the client is a recursive vim instance', function() + describe('recursive (child) nvim client', function() if os.getenv("TRAVIS") and helpers.os_name() == "osx" then -- XXX: Hangs Travis macOS since e9061117a5b8f195c3f26a5cb94e18ddd7752d86. pending("[Hangs on Travis macOS. #5002]", function() end) @@ -153,7 +171,7 @@ describe('server -> client', function() after_each(function() command('call rpcstop(vim)') end) - it('can send/recieve notifications and make requests', function() + it('can send/receive notifications and make requests', function() nvim('command', "call rpcnotify(vim, 'vim_set_current_line', 'SOME TEXT')") -- Wait for the notification to complete. @@ -182,11 +200,11 @@ describe('server -> client', function() it('returns an error if the request failed', function() local status, err = pcall(eval, "rpcrequest(vim, 'does-not-exist')") eq(false, status) - eq(true, string.match(err, ': (.*)') == 'Failed to evaluate expression') + ok(nil ~= string.match(err, 'Failed to evaluate expression')) end) end) - describe('when using jobstart', function() + describe('jobstart()', function() local jobid before_each(function() local channel = nvim('get_api_info')[1] @@ -202,7 +220,7 @@ describe('server -> client', function() \ 'rpc': v:true \ } ]]) - local lua_prog = arg[-1] + local lua_prog = Paths.test_lua_prg meths.set_var("args", {lua_prog, 'test/functional/api/rpc_fixture.lua'}) jobid = eval("jobstart(g:args, g:job_opts)") neq(0, 'jobid') @@ -212,6 +230,8 @@ describe('server -> client', function() funcs.jobstop(jobid) end) + if helpers.pending_win32(pending) then return end + it('rpc and text stderr can be combined', function() eq("ok",funcs.rpcrequest(jobid, "poll")) funcs.rpcnotify(jobid, "ping") @@ -223,4 +243,80 @@ describe('server -> client', function() end) end) + describe('connecting to another (peer) nvim', function() + local function connect_test(server, mode, address) + local serverpid = funcs.getpid() + local client = spawn(nvim_argv) + set_session(client, true) + local clientpid = funcs.getpid() + neq(serverpid, clientpid) + local id = funcs.sockconnect(mode, address, {rpc=true}) + ok(id > 0) + + funcs.rpcrequest(id, 'nvim_set_current_line', 'hello') + local client_id = funcs.rpcrequest(id, 'nvim_get_api_info')[1] + + set_session(server, true) + eq(serverpid, funcs.getpid()) + eq('hello', meths.get_current_line()) + + -- method calls work both ways + funcs.rpcrequest(client_id, 'nvim_set_current_line', 'howdy!') + eq(id, funcs.rpcrequest(client_id, 'nvim_get_api_info')[1]) + + set_session(client, true) + eq(clientpid, funcs.getpid()) + eq('howdy!', meths.get_current_line()) + + server:close() + client:close() + end + + it('via named pipe', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverlist()[1] + local first = string.sub(address,1,1) + ok(first == '/' or first == '\\') + connect_test(server, 'pipe', address) + end) + + it('via ip address', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverstart("127.0.0.1:") + eq('127.0.0.1:', string.sub(address,1,10)) + connect_test(server, 'tcp', address) + end) + + it('via hostname', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverstart("localhost:") + eq('localhost:', string.sub(address,1,10)) + connect_test(server, 'tcp', address) + end) + end) + + describe('connecting to its own pipe address', function() + it('does not deadlock', function() + if not os.getenv("TRAVIS") and helpers.os_name() == "osx" then + -- It does, in fact, deadlock on QuickBuild. #6851 + pending("deadlocks on QuickBuild", function() end) + return + end + local address = funcs.serverlist()[1] + local first = string.sub(address,1,1) + ok(first == '/' or first == '\\') + local serverpid = funcs.getpid() + + local id = funcs.sockconnect('pipe', address, {rpc=true}) + + funcs.rpcrequest(id, 'nvim_set_current_line', 'hello') + eq('hello', meths.get_current_line()) + eq(serverpid, funcs.rpcrequest(id, "nvim_eval", "getpid()")) + + eq(id, funcs.rpcrequest(id, 'nvim_get_api_info')[1]) + end) + end) end) diff --git a/test/functional/api/tabpage_spec.lua b/test/functional/api/tabpage_spec.lua index e10f30085f..260a91a80c 100644 --- a/test/functional/api/tabpage_spec.lua +++ b/test/functional/api/tabpage_spec.lua @@ -6,6 +6,8 @@ local curtabmeths = helpers.curtabmeths local funcs = helpers.funcs local request = helpers.request local NIL = helpers.NIL +local meth_pcall = helpers.meth_pcall +local command = helpers.command describe('api/tabpage', function() before_each(clear) @@ -32,6 +34,11 @@ describe('api/tabpage', function() eq(1, funcs.exists('t:lua')) curtabmeths.del_var('lua') eq(0, funcs.exists('t:lua')) + eq({false, 'Key does not exist: lua'}, meth_pcall(curtabmeths.del_var, 'lua')) + curtabmeths.set_var('lua', 1) + command('lockvar t:lua') + eq({false, 'Key is locked: lua'}, meth_pcall(curtabmeths.del_var, 'lua')) + eq({false, 'Key is locked: lua'}, meth_pcall(curtabmeths.set_var, 'lua', 1)) end) it('tabpage_set_var returns the old value', function() diff --git a/test/functional/api/version_spec.lua b/test/functional/api/version_spec.lua index 3efd00ddbe..d23f058f69 100644 --- a/test/functional/api/version_spec.lua +++ b/test/functional/api/version_spec.lua @@ -4,26 +4,16 @@ local clear, funcs, eq = helpers.clear, helpers.funcs, helpers.eq local function read_mpack_file(fname) local fd = io.open(fname, 'rb') + if fd == nil then + return nil + end + local data = fd:read('*a') fd:close() local unpack = mpack.Unpacker() return unpack(data) end --- Remove metadata that is not essential to backwards-compatibility. -local function remove_function_metadata(fspec) - fspec['can_fail'] = nil - fspec['async'] = nil - fspec['method'] = nil - fspec['since'] = nil - fspec['deprecated_since'] = nil - fspec['receives_channel_id'] = nil - for idx, _ in ipairs(fspec['parameters']) do - fspec['parameters'][idx][2] = '' -- Remove parameter name. - end - return fspec -end - describe("api_info()['version']", function() before_each(clear) @@ -49,23 +39,111 @@ describe("api_info()['version']", function() eq(0, funcs.has("nvim-"..major.."."..(minor + 1).."."..patch)) eq(0, funcs.has("nvim-"..(major + 1).."."..minor.."."..patch)) end) +end) + + +describe("api functions", function() + before_each(clear) + + local function func_table(metadata) + local functions = {} + for _,f in ipairs(metadata.functions) do + functions[f.name] = f + end + return functions + end + + -- Remove metadata that is not essential to backwards-compatibility. + local function filter_function_metadata(f) + f.deprecated_since = nil + for idx, _ in ipairs(f.parameters) do + f.parameters[idx][2] = '' -- Remove parameter name. + end + + if string.sub(f.name, 1, 4) ~= "nvim" then + f.method = nil + end + return f + end - it("api_compatible level is valid", function() - local api = helpers.call('api_info') - local compat = api['version']['api_compatible'] - local path = 'test/functional/fixtures/api_level_' - ..tostring(compat)..'.mpack' - - -- Verify that the current API function signatures match those of the API - -- level for which we claim compatibility. - local old_api = read_mpack_file(path) - for _, fn_old in ipairs(old_api['functions']) do - for _, fn_new in ipairs(api['functions']) do - if fn_old['name'] == fn_new['name'] then - eq(remove_function_metadata(fn_old), - remove_function_metadata(fn_new)) + -- Level 0 represents methods from 0.1.5 and earlier, when 'since' was not + -- yet defined, and metadata was not filtered of internal keys like 'async'. + local function clean_level_0(metadata) + for _, f in ipairs(metadata.functions) do + f.can_fail = nil + f.async = nil + f.receives_channel_id = nil + f.since = 0 + end + end + + it("are compatible with old metadata or have new level", function() + local api = helpers.call('api_info') + local compat = api.version.api_compatible + local api_level = api.version.api_level + local stable + if api.version.api_prerelease then + stable = api_level-1 + else + stable = api_level + end + + local funcs_new = func_table(api) + local funcs_compat = {} + for level = compat, stable do + local path = ('test/functional/fixtures/api_level_'.. + tostring(level)..'.mpack') + local old_api = read_mpack_file(path) + if old_api == nil then + local errstr = "missing metadata fixture for stable level "..level..". " + if level == api_level and not api.version.api_prerelease then + errstr = (errstr.."If NVIM_API_CURRENT was bumped, ".. + "don't forget to set NVIM_API_PRERELEASE to true.") + end + error(errstr) + end + + if level == 0 then + clean_level_0(old_api) + end + + for _,f in ipairs(old_api.functions) do + if funcs_new[f.name] == nil then + if f.since >= compat then + error('function '..f.name..' was removed but exists in level '.. + f.since..' which nvim should be compatible with') + end + else + eq(filter_function_metadata(f), + filter_function_metadata(funcs_new[f.name])) + end + end + + funcs_compat[level] = func_table(old_api) + end + + for _,f in ipairs(api.functions) do + if f.since <= stable then + local f_old = funcs_compat[f.since][f.name] + if f_old == nil then + if string.sub(f.name, 1, 4) == "nvim" then + local errstr = ("function "..f.name.." has too low since value. ".. + "For new functions set it to "..(stable+1)..".") + if not api.version.api_prerelease then + errstr = (errstr.." Also bump NVIM_API_CURRENT and set ".. + "NVIM_API_PRERELEASE to true in CMakeLists.txt.") + end + error(errstr) + else + error("function name '"..f.name.."' doesn't begin with 'nvim_'") + end end + elseif f.since > api_level then + error("function "..f.name.." has since value > api_level. ".. + "Please bump NVIM_API_CURRENT and set ".. + "NVIM_API_PRERELEASE to true in CMakeLists.txt.") end end end) + end) diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index ce6c52e334..a4b643589a 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -7,6 +7,8 @@ local os_name = helpers.os_name local meths = helpers.meths local funcs = helpers.funcs local request = helpers.request +local meth_pcall = helpers.meth_pcall +local command = helpers.command describe('api', function() before_each(clear) @@ -43,7 +45,7 @@ describe('api', function() it('works', function() nvim('command', 'let g:v1 = "a"') nvim('command', 'let g:v2 = [1, 2, {"v3": 3}]') - eq({v1 = 'a', v2 = {1, 2, {v3 = 3}}}, nvim('eval', 'g:')) + eq({v1 = 'a', v2 = { 1, 2, { v3 = 3 } } }, nvim('eval', 'g:')) end) it('handles NULL-initialized strings correctly', function() @@ -65,7 +67,7 @@ describe('api', function() describe('nvim_call_function', function() it('works', function() - nvim('call_function', 'setqflist', {{{ filename = 'something', lnum = 17}}, 'r'}) + nvim('call_function', 'setqflist', { { { filename = 'something', lnum = 17 } }, 'r' }) eq(17, nvim('call_function', 'getqflist', {})[1].lnum) eq(17, nvim('call_function', 'eval', {17})) eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'})) @@ -79,6 +81,36 @@ describe('api', function() end) end) + describe('nvim_execute_lua', function() + it('works', function() + meths.execute_lua('vim.api.nvim_set_var("test", 3)', {}) + eq(3, meths.get_var('test')) + + eq(17, meths.execute_lua('a, b = ...\nreturn a + b', {10,7})) + + eq(NIL, meths.execute_lua('function xx(a,b)\nreturn a..b\nend',{})) + eq("xy", meths.execute_lua('return xx(...)', {'x','y'})) + end) + + it('reports errors', function() + eq({false, 'Error loading lua: [string "<nvim>"]:1: '.. + "'=' expected near '+'"}, + meth_pcall(meths.execute_lua, 'a+*b', {})) + + eq({false, 'Error loading lua: [string "<nvim>"]:1: '.. + "unexpected symbol near '1'"}, + meth_pcall(meths.execute_lua, '1+2', {})) + + eq({false, 'Error loading lua: [string "<nvim>"]:1: '.. + "unexpected symbol"}, + meth_pcall(meths.execute_lua, 'aa=bb\0', {})) + + eq({false, 'Error executing lua: [string "<nvim>"]:1: '.. + "attempt to call global 'bork' (a nil value)"}, + meth_pcall(meths.execute_lua, 'bork()', {})) + end) + end) + describe('nvim_input', function() it("VimL error: does NOT fail, updates v:errmsg", function() local status, _ = pcall(nvim, "input", ":call bogus_fn()<CR>") @@ -117,6 +149,11 @@ describe('api', function() eq(1, funcs.exists('g:lua')) meths.del_var('lua') eq(0, funcs.exists('g:lua')) + eq({false, 'Key does not exist: lua'}, meth_pcall(meths.del_var, 'lua')) + meths.set_var('lua', 1) + command('lockvar lua') + eq({false, 'Key is locked: lua'}, meth_pcall(meths.del_var, 'lua')) + eq({false, 'Key is locked: lua'}, meth_pcall(meths.set_var, 'lua', 1)) end) it('vim_set_var returns the old value', function() @@ -146,6 +183,28 @@ describe('api', function() nvim('set_option', 'equalalways', false) ok(not nvim('get_option', 'equalalways')) end) + + it('works to get global value of local options', function() + eq(false, nvim('get_option', 'lisp')) + eq(8, nvim('get_option', 'shiftwidth')) + end) + + it('works to set global value of local options', function() + nvim('set_option', 'lisp', true) + eq(true, nvim('get_option', 'lisp')) + eq(false, helpers.curbuf('get_option', 'lisp')) + eq(nil, nvim('command_output', 'setglobal lisp?'):match('nolisp')) + eq('nolisp', nvim('command_output', 'setlocal lisp?'):match('nolisp')) + nvim('set_option', 'shiftwidth', 20) + eq('20', nvim('command_output', 'setglobal shiftwidth?'):match('%d+')) + eq('8', nvim('command_output', 'setlocal shiftwidth?'):match('%d+')) + end) + + it('most window-local options have no global value', function() + local status, err = pcall(nvim, 'get_option', 'foldcolumn') + eq(false, status) + ok(err:match('Invalid option name') ~= nil) + end) end) describe('nvim_{get,set}_current_buf, nvim_list_bufs', function() @@ -192,6 +251,152 @@ describe('api', function() end) end) + describe('nvim_get_mode', function() + it("during normal-mode `g` returns blocking=true", function() + nvim("input", "o") -- add a line + eq({mode='i', blocking=false}, nvim("get_mode")) + nvim("input", [[<C-\><C-N>]]) + eq(2, nvim("eval", "line('.')")) + eq({mode='n', blocking=false}, nvim("get_mode")) + + nvim("input", "g") + eq({mode='n', blocking=true}, nvim("get_mode")) + + nvim("input", "k") -- complete the operator + eq(1, nvim("eval", "line('.')")) -- verify the completed operator + eq({mode='n', blocking=false}, nvim("get_mode")) + end) + + it("returns the correct result multiple consecutive times", function() + for _ = 1,5 do + eq({mode='n', blocking=false}, nvim("get_mode")) + end + nvim("input", "g") + for _ = 1,4 do + eq({mode='n', blocking=true}, nvim("get_mode")) + end + nvim("input", "g") + for _ = 1,7 do + eq({mode='n', blocking=false}, nvim("get_mode")) + end + end) + + it("during normal-mode CTRL-W, returns blocking=true", function() + nvim("input", "<C-W>") + eq({mode='n', blocking=true}, nvim("get_mode")) + + nvim("input", "s") -- complete the operator + eq(2, nvim("eval", "winnr('$')")) -- verify the completed operator + eq({mode='n', blocking=false}, nvim("get_mode")) + end) + + it("during press-enter prompt returns blocking=true", function() + eq({mode='n', blocking=false}, nvim("get_mode")) + command("echom 'msg1'") + command("echom 'msg2'") + command("echom 'msg3'") + command("echom 'msg4'") + command("echom 'msg5'") + eq({mode='n', blocking=false}, nvim("get_mode")) + nvim("input", ":messages<CR>") + eq({mode='r', blocking=true}, nvim("get_mode")) + end) + + it("during getchar() returns blocking=false", function() + nvim("input", ":let g:test_input = nr2char(getchar())<CR>") + -- Events are enabled during getchar(), RPC calls are *not* blocked. #5384 + eq({mode='n', blocking=false}, nvim("get_mode")) + eq(0, nvim("eval", "exists('g:test_input')")) + nvim("input", "J") + eq("J", nvim("eval", "g:test_input")) + eq({mode='n', blocking=false}, nvim("get_mode")) + end) + + -- TODO: bug #6247#issuecomment-286403810 + it("batched with input", function() + eq({mode='n', blocking=false}, nvim("get_mode")) + command("echom 'msg1'") + command("echom 'msg2'") + command("echom 'msg3'") + command("echom 'msg4'") + command("echom 'msg5'") + + local req = { + {'nvim_get_mode', {}}, + {'nvim_input', {':messages<CR>'}}, + {'nvim_get_mode', {}}, + {'nvim_eval', {'1'}}, + } + eq({ { {mode='n', blocking=false}, + 13, + {mode='n', blocking=false}, -- TODO: should be blocked=true ? + 1 }, + NIL}, meths.call_atomic(req)) + eq({mode='r', blocking=true}, nvim("get_mode")) + end) + it("during insert-mode map-pending, returns blocking=true #6166", function() + command("inoremap xx foo") + nvim("input", "ix") + eq({mode='i', blocking=true}, nvim("get_mode")) + end) + it("during normal-mode gU, returns blocking=false #6166", function() + nvim("input", "gu") + eq({mode='no', blocking=false}, nvim("get_mode")) + end) + end) + + describe('RPC (K_EVENT) #6166', function() + it('does not complete ("interrupt") normal-mode operator-pending', function() + helpers.insert([[ + FIRST LINE + SECOND LINE]]) + nvim('input', 'gg') + nvim('input', 'gu') + -- Make any RPC request (can be non-async: op-pending does not block). + nvim('get_current_buf') + -- Buffer should not change. + helpers.expect([[ + FIRST LINE + SECOND LINE]]) + -- Now send input to complete the operator. + nvim('input', 'j') + helpers.expect([[ + first line + second line]]) + end) + it('does not complete ("interrupt") normal-mode map-pending', function() + command("nnoremap dd :let g:foo='it worked...'<CR>") + helpers.insert([[ + FIRST LINE + SECOND LINE]]) + nvim('input', 'gg') + nvim('input', 'd') + -- Make any RPC request (must be async, because map-pending blocks). + nvim('get_api_info') + -- Send input to complete the mapping. + nvim('input', 'd') + helpers.expect([[ + FIRST LINE + SECOND LINE]]) + eq('it worked...', helpers.eval('g:foo')) + end) + it('does not complete ("interrupt") insert-mode map-pending', function() + command('inoremap xx foo') + command('set timeoutlen=9999') + helpers.insert([[ + FIRST LINE + SECOND LINE]]) + nvim('input', 'ix') + -- Make any RPC request (must be async, because map-pending blocks). + nvim('get_api_info') + -- Send input to complete the mapping. + nvim('input', 'x') + helpers.expect([[ + FIRST LINE + SECOND LINfooE]]) + end) + end) + describe('nvim_replace_termcodes', function() it('escapes K_SPECIAL as K_SPECIAL KS_SPECIAL KE_FILLER', function() eq('\128\254X', helpers.nvim('replace_termcodes', '\128', true, true, true)) @@ -212,6 +417,27 @@ describe('api', function() eq('\128\253\44', helpers.nvim('replace_termcodes', '<LeftMouse>', true, true, true)) end) + + it('converts keycodes', function() + eq('\nx\27x\rx<x', helpers.nvim('replace_termcodes', + '<NL>x<Esc>x<CR>x<lt>x', true, true, true)) + end) + + it('does not convert keycodes if special=false', function() + eq('<NL>x<Esc>x<CR>x<lt>x', helpers.nvim('replace_termcodes', + '<NL>x<Esc>x<CR>x<lt>x', true, true, false)) + end) + + it('does not crash when transforming an empty string', function() + -- Actually does not test anything, because current code will use NULL for + -- an empty string. + -- + -- Problem here is that if String argument has .data in allocated memory + -- then `return str` in vim_replace_termcodes body will make Neovim free + -- `str.data` twice: once when freeing arguments, then when freeing return + -- value. + eq('', meths.replace_termcodes('', true, true, true)) + end) end) describe('nvim_feedkeys', function() @@ -220,13 +446,13 @@ describe('api', function() -- notice the special char(…) \xe2\80\xa6 nvim('feedkeys', ':let x1="…"\n', '', true) - -- Both replace_termcodes and feedkeys escape \x80 + -- Both nvim_replace_termcodes and nvim_feedkeys escape \x80 local inp = helpers.nvim('replace_termcodes', ':let x2="…"<CR>', true, true, true) - nvim('feedkeys', inp, '', true) + nvim('feedkeys', inp, '', true) -- escape_csi=true - -- Disabling CSI escaping in feedkeys + -- nvim_feedkeys with CSI escaping disabled inp = helpers.nvim('replace_termcodes', ':let x3="…"<CR>', true, true, true) - nvim('feedkeys', inp, '', false) + nvim('feedkeys', inp, '', false) -- escape_csi=false helpers.stop() end @@ -383,7 +609,7 @@ describe('api', function() eq(5, meths.get_var('avar')) end) - it('throws error on malformated arguments', function() + it('throws error on malformed arguments', function() local req = { {'nvim_set_var', {'avar', 1}}, {'nvim_set_var'}, @@ -396,7 +622,7 @@ describe('api', function() eq(1, meths.get_var('avar')) req = { - {'nvim_set_var', {'bvar', {2,3}}}, + { 'nvim_set_var', { 'bvar', { 2, 3 } } }, 12, } status, err = pcall(meths.call_atomic, req) @@ -410,20 +636,57 @@ describe('api', function() } status, err = pcall(meths.call_atomic, req) eq(false, status) - ok(err:match('args must be Array') ~= nil) + ok(err:match('Args must be Array') ~= nil) -- call before was done, but not after eq(1, meths.get_var('avar')) eq({''}, meths.buf_get_lines(0, 0, -1, true)) end) end) + describe('list_runtime_paths', function() + it('returns nothing with empty &runtimepath', function() + meths.set_option('runtimepath', '') + eq({}, meths.list_runtime_paths()) + end) + it('returns single runtimepath', function() + meths.set_option('runtimepath', 'a') + eq({'a'}, meths.list_runtime_paths()) + end) + it('returns two runtimepaths', function() + meths.set_option('runtimepath', 'a,b') + eq({'a', 'b'}, meths.list_runtime_paths()) + end) + it('returns empty strings when appropriate', function() + meths.set_option('runtimepath', 'a,,b') + eq({'a', '', 'b'}, meths.list_runtime_paths()) + meths.set_option('runtimepath', ',a,b') + eq({'', 'a', 'b'}, meths.list_runtime_paths()) + meths.set_option('runtimepath', 'a,b,') + eq({'a', 'b', ''}, meths.list_runtime_paths()) + end) + it('truncates too long paths', function() + local long_path = ('/a'):rep(8192) + meths.set_option('runtimepath', long_path) + local paths_list = meths.list_runtime_paths() + neq({long_path}, paths_list) + eq({long_path:sub(1, #(paths_list[1]))}, paths_list) + end) + end) + it('can throw exceptions', function() local status, err = pcall(nvim, 'get_option', 'invalid-option') eq(false, status) ok(err:match('Invalid option name') ~= nil) end) - it("doesn't leak memory on incorrect argument types", function() + it('does not truncate error message <1 MB #5984', function() + local very_long_name = 'A'..('x'):rep(10000)..'Z' + local status, err = pcall(nvim, 'get_option', very_long_name) + eq(false, status) + eq(very_long_name, err:match('Ax+Z?')) + end) + + it("does not leak memory on incorrect argument types", function() local status, err = pcall(nvim, 'set_current_dir',{'not', 'a', 'dir'}) eq(false, status) ok(err:match(': Wrong type for argument 1, expecting String') ~= nil) diff --git a/test/functional/api/window_spec.lua b/test/functional/api/window_spec.lua index 465bda6bc9..8a65d3f71e 100644 --- a/test/functional/api/window_spec.lua +++ b/test/functional/api/window_spec.lua @@ -8,6 +8,9 @@ local curwinmeths = helpers.curwinmeths local funcs = helpers.funcs local request = helpers.request local NIL = helpers.NIL +local meth_pcall = helpers.meth_pcall +local meths = helpers.meths +local command = helpers.command -- check if str is visible at the beginning of some line local function is_visible(str) @@ -53,6 +56,12 @@ describe('api/win', function() eq('typing\n some dumb text', curbuf_contents()) end) + it('does not leak memory when using invalid window ID with invalid pos', + function() + eq({false, 'Invalid window id'}, + meth_pcall(meths.win_set_cursor, 1, {"b\na"})) + end) + it('updates the screen, and also when the window is unfocused', function() insert("prologue") feed('100o<esc>') @@ -137,6 +146,11 @@ describe('api/win', function() eq(1, funcs.exists('w:lua')) curwinmeths.del_var('lua') eq(0, funcs.exists('w:lua')) + eq({false, 'Key does not exist: lua'}, meth_pcall(curwinmeths.del_var, 'lua')) + curwinmeths.set_var('lua', 1) + command('lockvar w:lua') + eq({false, 'Key is locked: lua'}, meth_pcall(curwinmeths.del_var, 'lua')) + eq({false, 'Key is locked: lua'}, meth_pcall(curwinmeths.set_var, 'lua', 1)) end) it('window_set_var returns the old value', function() |