aboutsummaryrefslogtreecommitdiff
path: root/test/functional/api
diff options
context:
space:
mode:
Diffstat (limited to 'test/functional/api')
-rw-r--r--test/functional/api/buffer_spec.lua52
-rw-r--r--test/functional/api/keymap_spec.lua310
-rw-r--r--test/functional/api/server_notifications_spec.lua8
-rw-r--r--test/functional/api/server_requests_spec.lua116
-rw-r--r--test/functional/api/tabpage_spec.lua7
-rw-r--r--test/functional/api/version_spec.lua134
-rw-r--r--test/functional/api/vim_spec.lua283
-rw-r--r--test/functional/api/window_spec.lua14
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()