diff options
Diffstat (limited to 'test')
62 files changed, 3193 insertions, 470 deletions
diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua new file mode 100644 index 0000000000..f3f9f93649 --- /dev/null +++ b/test/functional/api/command_spec.lua @@ -0,0 +1,80 @@ +local helpers = require('test.functional.helpers')(after_each) + +local NIL = helpers.NIL +local clear = helpers.clear +local command = helpers.command +local curbufmeths = helpers.curbufmeths +local eq = helpers.eq +local expect_err = helpers.expect_err +local meths = helpers.meths +local source = helpers.source + +describe('nvim_get_commands', function() + local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', range=NIL, register=false, script_id=0, } + local cmd_dict2 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='pwd', name='Pwd', nargs='?', range=NIL, register=false, script_id=0, } + before_each(clear) + + it('gets empty list if no commands were defined', function() + eq({}, meths.get_commands({builtin=false})) + end) + + it('validates input', function() + expect_err('builtin=true not implemented', meths.get_commands, + {builtin=true}) + expect_err('unexpected key: foo', meths.get_commands, + {foo='blah'}) + end) + + it('gets global user-defined commands', function() + -- Define a command. + command('command -nargs=1 Hello echo "Hello World"') + eq({Hello=cmd_dict}, meths.get_commands({builtin=false})) + -- Define another command. + command('command -nargs=? Pwd pwd'); + eq({Hello=cmd_dict, Pwd=cmd_dict2}, meths.get_commands({builtin=false})) + -- Delete a command. + command('delcommand Pwd') + eq({Hello=cmd_dict}, meths.get_commands({builtin=false})) + end) + + it('gets buffer-local user-defined commands', function() + -- Define a buffer-local command. + command('command -buffer -nargs=1 Hello echo "Hello World"') + eq({Hello=cmd_dict}, curbufmeths.get_commands({builtin=false})) + -- Define another buffer-local command. + command('command -buffer -nargs=? Pwd pwd') + eq({Hello=cmd_dict, Pwd=cmd_dict2}, curbufmeths.get_commands({builtin=false})) + -- Delete a command. + command('delcommand Pwd') + eq({Hello=cmd_dict}, curbufmeths.get_commands({builtin=false})) + + -- {builtin=true} always returns empty for buffer-local case. + eq({}, curbufmeths.get_commands({builtin=true})) + end) + + it('gets various command attributes', function() + local cmd0 = { addr='arguments', bang=false, bar=false, complete='dir', complete_arg=NIL, count='10', definition='pwd <args>', name='TestCmd', nargs='0', range='10', register=false, script_id=0, } + local cmd1 = { addr=NIL, bang=false, bar=false, complete='custom', complete_arg='ListUsers', count=NIL, definition='!finger <args>', name='Finger', nargs='+', range=NIL, register=false, script_id=1, } + local cmd2 = { addr=NIL, bang=true, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253Q2_foo(<q-args>)', name='Cmd2', nargs='*', range=NIL, register=false, script_id=2, } + local cmd3 = { addr=NIL, bang=false, bar=true, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253Q3_ohyeah()', name='Cmd3', nargs='0', range=NIL, register=false, script_id=3, } + local cmd4 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253Q4_just_great()', name='Cmd4', nargs='0', range=NIL, register=true, script_id=4, } + source([[ + command -complete=custom,ListUsers -nargs=+ Finger !finger <args> + ]]) + eq({Finger=cmd1}, meths.get_commands({builtin=false})) + command('command -complete=dir -addr=arguments -count=10 TestCmd pwd <args>') + eq({Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false})) + + source([[ + command -bang -nargs=* Cmd2 call <SID>foo(<q-args>) + ]]) + source([[ + command -bar -nargs=0 Cmd3 call <SID>ohyeah() + ]]) + source([[ + command -register Cmd4 call <SID>just_great() + ]]) + -- TODO(justinmk): Order is stable but undefined. Sort before return? + eq({Cmd2=cmd2, Cmd3=cmd3, Cmd4=cmd4, Finger=cmd1, TestCmd=cmd0}, meths.get_commands({builtin=false})) + end) +end) diff --git a/test/functional/api/highlight_spec.lua b/test/functional/api/highlight_spec.lua index 2297a0760f..fed53a3dfd 100644 --- a/test/functional/api/highlight_spec.lua +++ b/test/functional/api/highlight_spec.lua @@ -99,5 +99,14 @@ describe('highlight api',function() eq(false, err) eq('Invalid highlight name: ', string.match(emsg, 'Invalid.*')) + + -- Test "standout" attribute. #8054 + eq({ underline = true, }, + meths.get_hl_by_name('cursorline', 0)); + command('hi CursorLine cterm=standout,underline term=standout,underline gui=standout,underline') + command('set cursorline') + eq({ underline = true, standout = true, }, + meths.get_hl_by_name('cursorline', 0)); + end) end) diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua index 3a10f9c60f..f52372bee3 100644 --- a/test/functional/api/keymap_spec.lua +++ b/test/functional/api/keymap_spec.lua @@ -11,7 +11,7 @@ local source = helpers.source local shallowcopy = global_helpers.shallowcopy -describe('get_keymap', function() +describe('nvim_get_keymap', function() before_each(clear) -- Basic mapping and table to be used to describe results diff --git a/test/functional/api/proc_spec.lua b/test/functional/api/proc_spec.lua new file mode 100644 index 0000000000..d99c26b6c2 --- /dev/null +++ b/test/functional/api/proc_spec.lua @@ -0,0 +1,81 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local eq = helpers.eq +local funcs = helpers.funcs +local iswin = helpers.iswin +local nvim_argv = helpers.nvim_argv +local ok = helpers.ok +local request = helpers.request +local retry = helpers.retry +local NIL = helpers.NIL + +describe('api', function() + before_each(clear) + + describe('nvim_get_proc_children', function() + it('returns child process ids', function() + local this_pid = funcs.getpid() + + local job1 = funcs.jobstart(nvim_argv) + retry(nil, nil, function() + eq(1, #request('nvim_get_proc_children', this_pid)) + end) + + local job2 = funcs.jobstart(nvim_argv) + retry(nil, nil, function() + eq(2, #request('nvim_get_proc_children', this_pid)) + end) + + funcs.jobstop(job1) + retry(nil, nil, function() + eq(1, #request('nvim_get_proc_children', this_pid)) + end) + + funcs.jobstop(job2) + retry(nil, nil, function() + eq(0, #request('nvim_get_proc_children', this_pid)) + end) + end) + + it('validates input', function() + local status, rv = pcall(request, "nvim_get_proc_children", -1) + eq(false, status) + eq("Invalid pid: -1", string.match(rv, "Invalid.*")) + + status, rv = pcall(request, "nvim_get_proc_children", 0) + eq(false, status) + eq("Invalid pid: 0", string.match(rv, "Invalid.*")) + + -- Assume PID 99999 does not exist. + status, rv = pcall(request, "nvim_get_proc_children", 99999) + eq(true, status) + eq({}, rv) + end) + end) + + describe('nvim_get_proc', function() + it('returns process info', function() + local pid = funcs.getpid() + local pinfo = request('nvim_get_proc', pid) + eq((iswin() and 'nvim.exe' or 'nvim'), pinfo.name) + ok(pinfo.pid == pid) + ok(type(pinfo.ppid) == 'number' and pinfo.ppid ~= pid) + end) + + it('validates input', function() + local status, rv = pcall(request, "nvim_get_proc", -1) + eq(false, status) + eq("Invalid pid: -1", string.match(rv, "Invalid.*")) + + status, rv = pcall(request, "nvim_get_proc", 0) + eq(false, status) + eq("Invalid pid: 0", string.match(rv, "Invalid.*")) + + -- Assume PID 99999 does not exist. + status, rv = pcall(request, "nvim_get_proc", 99999) + eq(true, status) + eq(NIL, rv) + end) + end) +end) diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index 18229b54ff..e79a60fb10 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -11,6 +11,7 @@ local ok = helpers.ok local meths = helpers.meths local spawn, nvim_argv = helpers.spawn, helpers.nvim_argv local set_session = helpers.set_session +local expect_err = helpers.expect_err describe('server -> client', function() local cid @@ -221,9 +222,8 @@ describe('server -> client', function() end) it('returns an error if the request failed', function() - local status, err = pcall(eval, "rpcrequest(vim, 'does-not-exist')") - eq(false, status) - ok(nil ~= string.match(err, 'Failed to evaluate expression')) + expect_err('Vim:Invalid method name', + eval, "rpcrequest(vim, 'does-not-exist')") end) end) @@ -250,7 +250,7 @@ describe('server -> client', function() end) after_each(function() - funcs.jobstop(jobid) + pcall(funcs.jobstop, jobid) end) if helpers.pending_win32(pending) then return end @@ -261,7 +261,7 @@ describe('server -> client', function() eq({'notification', 'pong', {}}, next_msg()) eq("done!",funcs.rpcrequest(jobid, "write_stderr", "fluff\n")) eq({'notification', 'stderr', {0, {'fluff', ''}}}, next_msg()) - funcs.rpcrequest(jobid, "exit") + pcall(funcs.rpcrequest, jobid, "exit") eq({'notification', 'stderr', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -308,8 +308,8 @@ describe('server -> client', function() it('via ipv4 address', function() local server = spawn(nvim_argv) set_session(server) - local address = funcs.serverstart("127.0.0.1:") - if #address == 0 then + local status, address = pcall(funcs.serverstart, "127.0.0.1:") + if not status then pending('no ipv4 stack', function() end) return end @@ -320,8 +320,8 @@ describe('server -> client', function() it('via ipv6 address', function() local server = spawn(nvim_argv) set_session(server) - local address = funcs.serverstart('::1:') - if #address == 0 then + local status, address = pcall(funcs.serverstart, '::1:') + if not status then pending('no ipv6 stack', function() end) return end diff --git a/test/functional/api/ui_spec.lua b/test/functional/api/ui_spec.lua new file mode 100644 index 0000000000..b028a50b02 --- /dev/null +++ b/test/functional/api/ui_spec.lua @@ -0,0 +1,37 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear = helpers.clear +local eq = helpers.eq +local eval = helpers.eval +local expect_err = helpers.expect_err +local meths = helpers.meths +local request = helpers.request + +describe('nvim_ui_attach()', function() + before_each(function() + clear() + end) + it('handles very large width/height #2180', function() + local screen = Screen.new(999, 999) + screen:attach() + eq(999, eval('&lines')) + eq(999, eval('&columns')) + end) + it('invalid option returns error', function() + expect_err('No such UI option: foo', + meths.ui_attach, 80, 24, { foo={'foo'} }) + end) + it('validates channel arg', function() + expect_err('UI not attached to channel: 1', + request, 'nvim_ui_try_resize', 40, 10) + expect_err('UI not attached to channel: 1', + request, 'nvim_ui_set_option', 'rgb', true) + expect_err('UI not attached to channel: 1', + request, 'nvim_ui_detach') + + local screen = Screen.new() + screen:attach({rgb=false}) + expect_err('UI already attached to channel: 1', + request, 'nvim_ui_attach', 40, 10, { rgb=false }) + end) +end) diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index bd56161a54..71fa6632c7 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -4,17 +4,20 @@ local global_helpers = require('test.helpers') local NIL = helpers.NIL local clear, nvim, eq, neq = helpers.clear, helpers.nvim, helpers.eq, helpers.neq +local command = helpers.command +local eval = helpers.eval +local funcs = helpers.funcs +local iswin = helpers.iswin +local meth_pcall = helpers.meth_pcall +local meths = helpers.meths local ok, nvim_async, feed = helpers.ok, helpers.nvim_async, helpers.feed 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 -local iswin = helpers.iswin +local source = helpers.source -local intchar2lua = global_helpers.intchar2lua +local expect_err = global_helpers.expect_err local format_string = global_helpers.format_string +local intchar2lua = global_helpers.intchar2lua local mergedicts_copy = global_helpers.mergedicts_copy describe('api', function() @@ -38,20 +41,20 @@ describe('api', function() os.remove(fname) end) - it("parse error: fails (specific error), does NOT update v:errmsg", function() - -- Most API methods return generic errors (or no error) if a VimL - -- expression fails; nvim_command returns the VimL error details. + it('VimL validation error: fails with specific error', function() local status, rv = pcall(nvim, "command", "bogus_command") eq(false, status) -- nvim_command() failed. eq("E492:", string.match(rv, "E%d*:")) -- VimL error was returned. - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + eq('', nvim('eval', 'v:errmsg')) -- v:errmsg was not updated. + eq('', eval('v:exception')) end) - it("runtime error: fails (specific error)", function() + it('VimL execution error: fails with specific error', function() local status, rv = pcall(nvim, "command_output", "buffer 23487") eq(false, status) -- nvim_command() failed. eq("E86: Buffer 23487 does not exist", string.match(rv, "E%d*:.*")) - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + eq('', eval('v:errmsg')) -- v:errmsg was not updated. + eq('', eval('v:exception')) end) end) @@ -73,6 +76,8 @@ describe('api', function() it('captures command output', function() eq('this is\nspinal tap', nvim('command_output', [[echo "this is\nspinal tap"]])) + eq('no line ending!', + nvim('command_output', [[echon "no line ending!"]])) end) it('captures empty command output', function() @@ -105,21 +110,21 @@ describe('api', function() eq(':!echo foo\r\n\nfoo'..win_lf..'\n', nvim('command_output', [[!echo foo]])) end) - it("parse error: fails (specific error), does NOT update v:errmsg", function() + it('VimL validation error: fails with specific error', function() local status, rv = pcall(nvim, "command_output", "bogus commannnd") eq(false, status) -- nvim_command_output() failed. eq("E492: Not an editor command: bogus commannnd", string.match(rv, "E%d*:.*")) - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + eq('', eval('v:errmsg')) -- v:errmsg was not updated. -- Verify NO hit-enter prompt. eq({mode='n', blocking=false}, nvim("get_mode")) end) - it("runtime error: fails (specific error)", function() + it('VimL execution error: fails with specific error', function() local status, rv = pcall(nvim, "command_output", "buffer 42") eq(false, status) -- nvim_command_output() failed. eq("E86: Buffer 42 does not exist", string.match(rv, "E%d*:.*")) - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + eq('', eval('v:errmsg')) -- v:errmsg was not updated. -- Verify NO hit-enter prompt. eq({mode='n', blocking=false}, nvim("get_mode")) end) @@ -141,11 +146,10 @@ describe('api', function() eq(2, request("vim_eval", "1+1")) end) - it("VimL error: fails (generic error), does NOT update v:errmsg", function() - local status, rv = pcall(nvim, "eval", "bogus expression") - eq(false, status) -- nvim_eval() failed. - ok(nil ~= string.find(rv, "Failed to evaluate expression")) - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + it("VimL error: returns error details, does NOT update v:errmsg", function() + expect_err('E121: Undefined variable: bogus', request, + 'nvim_eval', 'bogus expression') + eq('', eval('v:errmsg')) -- v:errmsg was not updated. end) end) @@ -157,11 +161,100 @@ describe('api', function() eq('foo', nvim('call_function', 'simplify', {'this/./is//redundant/../../../foo'})) end) - it("VimL error: fails (generic error), does NOT update v:errmsg", function() - local status, rv = pcall(nvim, "call_function", "bogus function", {"arg1"}) - eq(false, status) -- nvim_call_function() failed. - ok(nil ~= string.find(rv, "Error calling function")) - eq("", nvim("eval", "v:errmsg")) -- v:errmsg was not updated. + it("VimL validation error: returns specific error, does NOT update v:errmsg", function() + expect_err('E117: Unknown function: bogus function', request, + 'nvim_call_function', 'bogus function', {'arg1'}) + expect_err('E119: Not enough arguments for function: atan', request, + 'nvim_call_function', 'atan', {}) + eq('', eval('v:exception')) + eq('', eval('v:errmsg')) -- v:errmsg was not updated. + end) + + it("VimL error: returns error details, does NOT update v:errmsg", function() + expect_err('E808: Number or Float required', request, + 'nvim_call_function', 'atan', {'foo'}) + expect_err('Invalid channel stream "xxx"', request, + 'nvim_call_function', 'chanclose', {999, 'xxx'}) + expect_err('E900: Invalid channel id', request, + 'nvim_call_function', 'chansend', {999, 'foo'}) + eq('', eval('v:exception')) + eq('', eval('v:errmsg')) -- v:errmsg was not updated. + end) + + it("VimL exception: returns exception details, does NOT update v:errmsg", function() + source([[ + function! Foo() abort + throw 'wtf' + endfunction + ]]) + expect_err('wtf', request, + 'nvim_call_function', 'Foo', {}) + eq('', eval('v:exception')) + eq('', eval('v:errmsg')) -- v:errmsg was not updated. + end) + + it('validates args', function() + local too_many_args = { 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x' } + source([[ + function! Foo(...) abort + echo a:000 + endfunction + ]]) + -- E740 + expect_err('Function called with too many arguments', request, + 'nvim_call_function', 'Foo', too_many_args) + end) + end) + + describe('nvim_call_dict_function', function() + it('invokes VimL dict function', function() + source([[ + function! F(name) dict + return self.greeting.', '.a:name.'!' + endfunction + let g:test_dict_fn = { 'greeting':'Hello', 'F':function('F') } + + let g:test_dict_fn2 = { 'greeting':'Hi' } + function g:test_dict_fn2.F2(name) + return self.greeting.', '.a:name.' ...' + endfunction + ]]) + + -- :help Dictionary-function + eq('Hello, World!', nvim('call_dict_function', 'g:test_dict_fn', 'F', {'World'})) + -- Funcref is sent as NIL over RPC. + eq({ greeting = 'Hello', F = NIL }, nvim('get_var', 'test_dict_fn')) + + -- :help numbered-function + eq('Hi, Moon ...', nvim('call_dict_function', 'g:test_dict_fn2', 'F2', {'Moon'})) + -- Funcref is sent as NIL over RPC. + eq({ greeting = 'Hi', F2 = NIL }, nvim('get_var', 'test_dict_fn2')) + + -- Function specified via RPC dict. + source('function! G() dict\n return "@".(self.result)."@"\nendfunction') + eq('@it works@', nvim('call_dict_function', { result = 'it works', G = 'G'}, 'G', {})) + end) + + it('validates args', function() + command('let g:d={"baz":"zub","meep":[]}') + expect_err('Not found: bogus', request, + 'nvim_call_dict_function', 'g:d', 'bogus', {1,2}) + expect_err('Not a function: baz', request, + 'nvim_call_dict_function', 'g:d', 'baz', {1,2}) + expect_err('Not a function: meep', request, + 'nvim_call_dict_function', 'g:d', 'meep', {1,2}) + expect_err('E117: Unknown function: f', request, + 'nvim_call_dict_function', { f = '' }, 'f', {1,2}) + expect_err('Not a function: f', request, + 'nvim_call_dict_function', "{ 'f': '' }", 'f', {1,2}) + expect_err('dict argument type must be String or Dictionary', request, + 'nvim_call_dict_function', 42, 'f', {1,2}) + expect_err('Failed to evaluate dict expression', request, + 'nvim_call_dict_function', 'foo', 'f', {1,2}) + expect_err('dict not found', request, + 'nvim_call_dict_function', '42', 'f', {1,2}) + expect_err('Invalid %(empty%) function name', request, + 'nvim_call_dict_function', "{ 'f': '' }", '', {1,2}) end) end) @@ -289,6 +382,21 @@ describe('api', function() eq(false, status) ok(err:match('Invalid option name') ~= nil) end) + + it('updates where the option was last set from', function() + nvim('set_option', 'equalalways', false) + local status, rv = pcall(nvim, 'command_output', + 'verbose set equalalways?') + eq(true, status) + ok(nil ~= string.find(rv, 'noequalalways\n'.. + '\tLast set from API client %(channel id %d+%)')) + + nvim('execute_lua', 'vim.api.nvim_set_option("equalalways", true)', {}) + status, rv = pcall(nvim, 'command_output', + 'verbose set equalalways?') + eq(true, status) + eq(' equalalways\n\tLast set from Lua', rv) + end) end) describe('nvim_{get,set}_current_buf, nvim_list_bufs', function() @@ -579,7 +687,8 @@ describe('api', function() screen:set_default_attr_ids({ [0] = {bold=true, foreground=Screen.colors.Blue}, [1] = {foreground = Screen.colors.White, background = Screen.colors.Red}, - [2] = {bold = true, foreground = Screen.colors.SeaGreen} + [2] = {bold = true, foreground = Screen.colors.SeaGreen}, + [3] = {bold = true, reverse = true}, }) end) @@ -600,11 +709,11 @@ describe('api', function() it('shows return prompt when more than &cmdheight lines', function() nvim_async('err_write', 'something happened\nvery bad\n') screen:expect([[ + | {0:~ }| {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| + {3: }| {1:something happened} | {1:very bad} | {2:Press ENTER or type command to continue}^ | @@ -614,9 +723,9 @@ describe('api', function() it('shows return prompt after all lines are shown', function() nvim_async('err_write', 'FAILURE\nERROR\nEXCEPTION\nTRACEBACK\n') screen:expect([[ + | {0:~ }| - {0:~ }| - {0:~ }| + {3: }| {1:FAILURE} | {1:ERROR} | {1:EXCEPTION} | @@ -644,11 +753,11 @@ describe('api', function() -- shows up to &cmdheight lines nvim_async('err_write', 'more fail\ntoo fail\n') screen:expect([[ + | {0:~ }| {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| + {3: }| {1:more fail} | {1:too fail} | {2:Press ENTER or type command to continue}^ | diff --git a/test/functional/autocmd/cmdline_spec.lua b/test/functional/autocmd/cmdline_spec.lua index 8ea086fb46..3f0504d02f 100644 --- a/test/functional/autocmd/cmdline_spec.lua +++ b/test/functional/autocmd/cmdline_spec.lua @@ -59,24 +59,25 @@ describe('cmdline autocommands', function() [1] = {bold = true, foreground = Screen.colors.Blue1}, [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [4] = {bold = true, reverse = true}, }) command("autocmd CmdlineEnter * echoerr 'FAIL'") command("autocmd CmdlineLeave * echoerr 'very error'") feed(':') screen:expect([[ + | {1:~ }| {1:~ }| {1:~ }| - {1:~ }| - {1:~ }| + {4: }| : | {2:E5500: autocmd has thrown an exception: Vim(echoerr):FAIL} | :^ | ]]) feed("put ='lorem ipsum'<cr>") screen:expect([[ - {1:~ }| - {1:~ }| + | + {4: }| : | {2:E5500: autocmd has thrown an exception: Vim(echoerr):FAIL} | :put ='lorem ipsum' | diff --git a/test/functional/autocmd/dirchanged_spec.lua b/test/functional/autocmd/dirchanged_spec.lua index 63cf0bc410..7979e91a4c 100644 --- a/test/functional/autocmd/dirchanged_spec.lua +++ b/test/functional/autocmd/dirchanged_spec.lua @@ -154,4 +154,11 @@ describe('autocmd DirChanged', function() eq('Failed to change directory', string.match(err, ': (.*)')) eq({cwd=dirs[2], scope='global'}, eval('g:ev')) end) + + it('works when local to buffer', function() + command('let g:triggered = 0') + command('autocmd DirChanged <buffer> let g:triggered = 1') + command('cd '..dirs[1]) + eq(1, eval('g:triggered')) + end) end) diff --git a/test/functional/autocmd/termclose_spec.lua b/test/functional/autocmd/termclose_spec.lua index 9918cbe4fa..db4e5379d0 100644 --- a/test/functional/autocmd/termclose_spec.lua +++ b/test/functional/autocmd/termclose_spec.lua @@ -1,3 +1,4 @@ +local luv = require('luv') local helpers = require('test.functional.helpers')(after_each) local clear, command, nvim, nvim_dir = @@ -31,7 +32,7 @@ describe('TermClose event', function() end) it('kills job trapping SIGTERM', function() - if helpers.pending_win32(pending) then return end + if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') command([[ let g:test_job = jobstart('trap "" TERM && echo 1 && sleep 60', { ]] @@ -39,17 +40,19 @@ describe('TermClose event', function() .. [[ 'on_exit': {-> execute('let g:test_job_exited = 1')}}) ]]) retry(nil, nil, function() eq(1, eval('get(g:, "test_job_started", 0)')) end) - local start = os.time() + luv.update_time() + local start = luv.now() command('call jobstop(g:test_job)') retry(nil, nil, function() eq(1, eval('get(g:, "test_job_exited", 0)')) end) - local duration = os.time() - start - -- nvim starts sending SIGTERM after KILL_TIMEOUT_MS - ok(duration >= 2) - ok(duration <= 4) -- <= 2 + delta because of slow CI + luv.update_time() + local duration = luv.now() - start + -- Nvim begins SIGTERM after KILL_TIMEOUT_MS. + ok(duration >= 2000) + ok(duration <= 4000) -- Epsilon for slow CI end) - it('kills pty job trapping SIGHUP and SIGTERM', function() - if helpers.pending_win32(pending) then return end + it('kills PTY job trapping SIGHUP and SIGTERM', function() + if iswin() then return end nvim('set_option', 'shell', 'sh') nvim('set_option', 'shellcmdflag', '-c') command([[ let g:test_job = jobstart('trap "" HUP TERM && echo 1 && sleep 60', { ]] @@ -58,13 +61,15 @@ describe('TermClose event', function() .. [[ 'on_exit': {-> execute('let g:test_job_exited = 1')}}) ]]) retry(nil, nil, function() eq(1, eval('get(g:, "test_job_started", 0)')) end) - local start = os.time() + luv.update_time() + local start = luv.now() command('call jobstop(g:test_job)') retry(nil, nil, function() eq(1, eval('get(g:, "test_job_exited", 0)')) end) - local duration = os.time() - start - -- nvim starts sending kill after 2*KILL_TIMEOUT_MS - ok(duration >= 4) - ok(duration <= 7) -- <= 4 + delta because of slow CI + luv.update_time() + local duration = luv.now() - start + -- Nvim begins SIGKILL after (2 * KILL_TIMEOUT_MS). + ok(duration >= 4000) + ok(duration <= 7000) -- Epsilon for slow CI end) it('reports the correct <abuf>', function() diff --git a/test/functional/clipboard/clipboard_provider_spec.lua b/test/functional/clipboard/clipboard_provider_spec.lua index a3ea3b568f..a40c080a6d 100644 --- a/test/functional/clipboard/clipboard_provider_spec.lua +++ b/test/functional/clipboard/clipboard_provider_spec.lua @@ -83,7 +83,14 @@ local function basic_register_test(noblock) end describe('clipboard', function() - before_each(clear) + local screen + + before_each(function() + clear() + screen = Screen.new(72, 4) + screen:attach() + command("set display-=msgsep") + end) it('unnamed register works without provider', function() eq('"', eval('v:register')) @@ -92,8 +99,6 @@ describe('clipboard', function() it('`:redir @+>` with invalid g:clipboard shows exactly one error #7184', function() - local screen = Screen.new(72, 4) - screen:attach() command("let g:clipboard = 'bogus'") feed_command('redir @+> | :silent echo system("cat CONTRIBUTING.md") | redir END') screen:expect([[ @@ -106,8 +111,6 @@ describe('clipboard', function() it('`:redir @+>|bogus_cmd|redir END` + invalid g:clipboard must not recurse #7184', function() - local screen = Screen.new(72, 4) - screen:attach() command("let g:clipboard = 'bogus'") feed_command('redir @+> | bogus_cmd | redir END') screen:expect([[ @@ -123,8 +126,6 @@ describe('clipboard', function() eq('', eval('provider#clipboard#Executable()')) eq('clipboard: invalid g:clipboard', eval('provider#clipboard#Error()')) - local screen = Screen.new(72, 4) - screen:attach() command("let g:clipboard = 'bogus'") -- Explicit clipboard attempt, should show a hint message. feed_command('let @+="foo"') @@ -493,10 +494,10 @@ describe('clipboard', function() feed_command("let g:test_clip['+'] = ['such', 'plus', 'stuff']") feed_command("registers") screen:expect([[ - ~ | - ~ | - ~ | - ~ | + | + {0:~ }| + {0:~ }| + {4: }| :registers | {1:--- Registers ---} | "* some{2:^J}star data{2:^J} | @@ -504,10 +505,11 @@ describe('clipboard', function() ": let g:test_clip['+'] = ['such', 'plus', 'stuff'] | {3:Press ENTER or type command to continue}^ | ]], { + [0] = {bold = true, foreground = Screen.colors.Blue}, [1] = {bold = true, foreground = Screen.colors.Fuchsia}, [2] = {foreground = Screen.colors.Blue}, - [3] = {bold = true, foreground = Screen.colors.SeaGreen}}, - {{bold = true, foreground = Screen.colors.Blue}}) + [3] = {bold = true, foreground = Screen.colors.SeaGreen}, + [4] = {bold = true, reverse = true}}) feed('<cr>') -- clear out of Press ENTER screen end) diff --git a/test/functional/core/exit_spec.lua b/test/functional/core/exit_spec.lua index 188a6a2c11..80c65e4544 100644 --- a/test/functional/core/exit_spec.lua +++ b/test/functional/core/exit_spec.lua @@ -1,6 +1,7 @@ local helpers = require('test.functional.helpers')(after_each) local command = helpers.command +local feed_command = helpers.feed_command local eval = helpers.eval local eq = helpers.eq local run = helpers.run @@ -33,6 +34,18 @@ describe('v:exiting', function() end run(on_request, nil, on_setup) end) + it('is 0 on exit from ex-mode involving try-catch', function() + local function on_setup() + command('autocmd VimLeavePre * call rpcrequest('..cid..', "")') + command('autocmd VimLeave * call rpcrequest('..cid..', "")') + feed_command('call feedkey("Q")','try', 'call NoFunction()', 'catch', 'echo "bye"', 'endtry', 'quit') + end + local function on_request() + eq(0, eval('v:exiting')) + return '' + end + run(on_request, nil, on_setup) + end) end) describe(':cquit', function() diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua new file mode 100644 index 0000000000..09533e4e60 --- /dev/null +++ b/test/functional/core/fileio_spec.lua @@ -0,0 +1,68 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local feed = helpers.feed +local funcs = helpers.funcs +local nvim_prog = helpers.nvim_prog +local request = helpers.request +local retry = helpers.retry +local rmdir = helpers.rmdir +local sleep = helpers.sleep + +describe('fileio', function() + before_each(function() + end) + after_each(function() + command(':qall!') + os.remove('Xtest_startup_shada') + os.remove('Xtest_startup_file1') + os.remove('Xtest_startup_file2') + rmdir('Xtest_startup_swapdir') + end) + + it('fsync() codepaths #8304', function() + clear({ args={ '-i', 'Xtest_startup_shada', + '--cmd', 'set directory=Xtest_startup_swapdir' } }) + + -- These cases ALWAYS force fsync (regardless of 'fsync' option): + + -- 1. Idle (CursorHold) with modified buffers (+ 'swapfile'). + command('write Xtest_startup_file1') + feed('ifoo<esc>h') + command('write') + eq(0, request('nvim__stats').fsync) -- 'nofsync' is the default. + command('set swapfile') + command('set updatetime=1') + feed('izub<esc>h') -- File is 'modified'. + sleep(3) -- Allow 'updatetime' to expire. + retry(3, nil, function() + eq(1, request('nvim__stats').fsync) + end) + command('set updatetime=9999') + + -- 2. Exit caused by deadly signal (+ 'swapfile'). + local j = funcs.jobstart({ nvim_prog, '-u', 'NONE', '-i', + 'Xtest_startup_shada', '--headless', + '-c', 'set swapfile', + '-c', 'write Xtest_startup_file2', + '-c', 'put =localtime()', }) + sleep(10) -- Let Nvim start. + funcs.jobstop(j) -- Send deadly signal. + + -- 3. SIGPWR signal. + -- ?? + + -- 4. Explicit :preserve command. + command('preserve') + eq(2, request('nvim__stats').fsync) + + -- 5. Enable 'fsync' option, write file. + command('set fsync') + feed('ibaz<esc>h') + command('write') + eq(4, request('nvim__stats').fsync) + end) +end) + diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index c5326aedfe..e90339b0cd 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -6,6 +6,10 @@ local clear, eq, eval, exc_exec, feed_command, feed, insert, neq, next_msg, nvim helpers.nvim_dir, helpers.ok, helpers.source, helpers.write_file, helpers.mkdir, helpers.rmdir local command = helpers.command +local funcs = helpers.funcs +local retry = helpers.retry +local meths = helpers.meths +local NIL = helpers.NIL local wait = helpers.wait local iswin = helpers.iswin local get_pathsep = helpers.get_pathsep @@ -15,14 +19,18 @@ local expect_twostreams = helpers.expect_twostreams local expect_msg_seq = helpers.expect_msg_seq local Screen = require('test.functional.ui.screen') +-- Kill process with given pid +local function os_kill(pid) + return os.execute((iswin() + and 'taskkill /f /t /pid '..pid..' > nul' + or 'kill -9 '..pid..' > /dev/null')) +end + describe('jobs', function() local channel before_each(function() clear() - if iswin() then - helpers.set_shell_powershell() - end channel = nvim('get_api_info')[1] nvim('set_var', 'channel', channel) source([[ @@ -48,7 +56,7 @@ describe('jobs', function() it('uses &shell and &shellcmdflag if passed a string', function() nvim('command', "let $VAR = 'abc'") if iswin() then - nvim('command', "let j = jobstart('echo $env:VAR', g:job_opts)") + nvim('command', "let j = jobstart('echo %VAR%', g:job_opts)") else nvim('command', "let j = jobstart('echo $VAR', g:job_opts)") end @@ -60,7 +68,7 @@ describe('jobs', function() it('changes to given / directory', function() nvim('command', "let g:job_opts.cwd = '/'") if iswin() then - nvim('command', "let j = jobstart('(Get-Location).Path', g:job_opts)") + nvim('command', "let j = jobstart('cd', g:job_opts)") else nvim('command', "let j = jobstart('pwd', g:job_opts)") end @@ -75,7 +83,7 @@ describe('jobs', function() mkdir(dir) nvim('command', "let g:job_opts.cwd = '" .. dir .. "'") if iswin() then - nvim('command', "let j = jobstart('(Get-Location).Path', g:job_opts)") + nvim('command', "let j = jobstart('cd', g:job_opts)") else nvim('command', "let j = jobstart('pwd', g:job_opts)") end @@ -99,7 +107,7 @@ describe('jobs', function() local _, err = pcall(function() nvim('command', "let g:job_opts.cwd = '" .. dir .. "'") if iswin() then - nvim('command', "let j = jobstart('pwd|%{$_.Path}', g:job_opts)") + nvim('command', "let j = jobstart('cd', g:job_opts)") else nvim('command', "let j = jobstart('pwd', g:job_opts)") end @@ -130,10 +138,8 @@ describe('jobs', function() end) it('invokes callbacks when the job writes and exits', function() - -- TODO: hangs on Windows - if helpers.pending_win32(pending) then return end nvim('command', "let g:job_opts.on_stderr = function('OnEvent')") - nvim('command', [[call jobstart('echo ""', g:job_opts)]]) + nvim('command', [[call jobstart(has('win32') ? 'echo:' : 'echo', g:job_opts)]]) expect_twostreams({{'notification', 'stdout', {0, {'', ''}}}, {'notification', 'stdout', {0, {''}}}}, {{'notification', 'stderr', {0, {''}}}}) @@ -141,15 +147,28 @@ describe('jobs', function() end) it('allows interactive commands', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. - nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") + nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") neq(0, eval('j')) nvim('command', 'call jobsend(j, "abc\\n")') eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg()) nvim('command', 'call jobsend(j, "123\\nxyz\\n")') - eq({'notification', 'stdout', {0, {'123', 'xyz', ''}}}, next_msg()) + expect_msg_seq( + { {'notification', 'stdout', {0, {'123', 'xyz', ''}}} + }, + -- Alternative sequence: + { {'notification', 'stdout', {0, {'123', ''}}}, + {'notification', 'stdout', {0, {'xyz', ''}}} + } + ) nvim('command', 'call jobsend(j, [123, "xyz", ""])') - eq({'notification', 'stdout', {0, {'123', 'xyz', ''}}}, next_msg()) + expect_msg_seq( + { {'notification', 'stdout', {0, {'123', 'xyz', ''}}} + }, + -- Alternative sequence: + { {'notification', 'stdout', {0, {'123', ''}}}, + {'notification', 'stdout', {0, {'xyz', ''}}} + } + ) nvim('command', "call jobstop(j)") eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) @@ -222,16 +241,14 @@ describe('jobs', function() end) it('closes the job streams with jobclose', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. - nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") + nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") nvim('command', 'call jobclose(j, "stdin")') eq({'notification', 'stdout', {0, {''}}}, next_msg()) - eq({'notification', 'exit', {0, 0}}, next_msg()) + eq({'notification', 'exit', {0, iswin() and 1 or 0}}, next_msg()) end) it("disallows jobsend on a job that closed stdin", function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. - nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") + nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") nvim('command', 'call jobclose(j, "stdin")') eq(false, pcall(function() nvim('command', 'call jobsend(j, ["some data"])') @@ -244,8 +261,7 @@ describe('jobs', function() end) it('disallows jobstop twice on the same job', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. - nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") + nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") neq(0, eval('j')) eq(true, pcall(eval, "jobstop(j)")) eq(false, pcall(eval, "jobstop(j)")) @@ -256,41 +272,49 @@ describe('jobs', function() end) it('can get the pid value using getpid', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. - nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") + nvim('command', "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") local pid = eval('jobpid(j)') - eq(0,os.execute('ps -p '..pid..' > /dev/null')) + neq(NIL, meths.get_proc(pid)) nvim('command', 'call jobstop(j)') eq({'notification', 'stdout', {0, {''}}}, next_msg()) - eq({'notification', 'exit', {0, 0}}, next_msg()) - neq(0,os.execute('ps -p '..pid..' > /dev/null')) + if iswin() then + expect_msg_seq( + -- win64 + { {'notification', 'exit', {0, 1}} + }, + -- win32 + { {'notification', 'exit', {0, 15}} + } + ) + else + eq({'notification', 'exit', {0, 0}}, next_msg()) + end + eq(NIL, meths.get_proc(pid)) end) it("do not survive the exit of nvim", function() - if helpers.pending_win32(pending) then return end -- use sleep, which doesn't die on stdin close - nvim('command', "let g:j = jobstart(['sleep', '1000'], g:job_opts)") + nvim('command', "let g:j = jobstart(has('win32') ? ['ping', '-n', '1001', '127.0.0.1'] : ['sleep', '1000'], g:job_opts)") local pid = eval('jobpid(g:j)') - eq(0,os.execute('ps -p '..pid..' > /dev/null')) + neq(NIL, meths.get_proc(pid)) clear() - neq(0,os.execute('ps -p '..pid..' > /dev/null')) + eq(NIL, meths.get_proc(pid)) end) it('can survive the exit of nvim with "detach"', function() - if helpers.pending_win32(pending) then return end nvim('command', 'let g:job_opts.detach = 1') - nvim('command', "let g:j = jobstart(['sleep', '1000'], g:job_opts)") + nvim('command', "let g:j = jobstart(has('win32') ? ['ping', '-n', '1001', '127.0.0.1'] : ['sleep', '1000'], g:job_opts)") local pid = eval('jobpid(g:j)') - eq(0,os.execute('ps -p '..pid..' > /dev/null')) + neq(NIL, meths.get_proc(pid)) clear() - eq(0,os.execute('ps -p '..pid..' > /dev/null')) + neq(NIL, meths.get_proc(pid)) -- clean up after ourselves - os.execute('kill -9 '..pid..' > /dev/null') + eq(0, os_kill(pid)) end) it('can pass user data to the callback', function() nvim('command', 'let g:job_opts.user = {"n": 5, "s": "str", "l": [1]}') - nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) + nvim('command', [[call jobstart('echo foo', g:job_opts)]]) local data = {n = 5, s = 'str', l = {1}} expect_msg_seq( { {'notification', 'stdout', {data, {'foo', ''}}}, @@ -308,14 +332,14 @@ describe('jobs', function() it('can omit data callbacks', function() nvim('command', 'unlet g:job_opts.on_stdout') nvim('command', 'let g:job_opts.user = 5') - nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) + nvim('command', [[call jobstart('echo foo', g:job_opts)]]) eq({'notification', 'exit', {5, 0}}, next_msg()) end) it('can omit exit callback', function() nvim('command', 'unlet g:job_opts.on_exit') nvim('command', 'let g:job_opts.user = 5') - nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) + nvim('command', [[call jobstart('echo foo', g:job_opts)]]) expect_msg_seq( { {'notification', 'stdout', {5, {'foo', ''} } }, {'notification', 'stdout', {5, {''} } }, @@ -397,15 +421,14 @@ describe('jobs', function() end) it('does not repeat output with slow output handlers', function() - if helpers.pending_win32(pending) then return end source([[ let d = {'data': []} function! d.on_stdout(job, data, event) dict - call add(self.data, a:data) + call add(self.data, Normalize(a:data)) sleep 200m endfunction if has('win32') - let cmd = '1,2,3,4,5 | foreach-object -process {echo $_; sleep 0.1}' + let cmd = 'for /L %I in (1,1,5) do @(echo %I& ping -n 2 127.0.0.1 > nul)' else let cmd = ['sh', '-c', 'for i in $(seq 1 5); do echo $i; sleep 0.1; done'] endif @@ -424,7 +447,7 @@ describe('jobs', function() endfunction let Callback = function('PrintArgs', ["foo", "bar"]) let g:job_opts = {'on_stdout': Callback} - call jobstart('echo "some text"', g:job_opts) + call jobstart('echo some text', g:job_opts) ]]) expect_msg_seq( { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, @@ -444,7 +467,7 @@ describe('jobs', function() return {id, data, event -> rpcnotify(g:channel, '1', a1, a2, Normalize(data), event)} endfun let g:job_opts = {'on_stdout': MkFun()} - call jobstart('echo "some text"', g:job_opts) + call jobstart('echo some text', g:job_opts) ]]) expect_msg_seq( { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, @@ -459,7 +482,7 @@ describe('jobs', function() it('jobstart() works when closure passed directly to `jobstart`', function() source([[ let g:job_opts = {'on_stdout': {id, data, event -> rpcnotify(g:channel, '1', 'foo', 'bar', Normalize(data), event)}} - call jobstart('echo "some text"', g:job_opts) + call jobstart('echo some text', g:job_opts) ]]) expect_msg_seq( { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, @@ -472,9 +495,20 @@ describe('jobs', function() end) describe('jobwait', function() + before_each(function() + if iswin() then + helpers.set_shell_powershell() + end + end) + it('returns a list of status codes', function() source([[ - call rpcnotify(g:channel, 'wait', jobwait([ + call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ + \ jobstart('Start-Sleep -Milliseconds 100; exit 4'), + \ jobstart('Start-Sleep -Milliseconds 300; exit 5'), + \ jobstart('Start-Sleep -Milliseconds 500; exit 6'), + \ jobstart('Start-Sleep -Milliseconds 700; exit 7') + \ ] : [ \ jobstart('sleep 0.10; exit 4'), \ jobstart('sleep 0.110; exit 5'), \ jobstart('sleep 0.210; exit 6'), @@ -494,7 +528,12 @@ describe('jobs', function() endif let g:exits += 1 endfunction - call jobwait([ + call jobwait(has('win32') ? [ + \ jobstart('Start-Sleep -Milliseconds 100; exit 5', g:dict), + \ jobstart('Start-Sleep -Milliseconds 300; exit 5', g:dict), + \ jobstart('Start-Sleep -Milliseconds 500; exit 5', g:dict), + \ jobstart('Start-Sleep -Milliseconds 700; exit 5', g:dict) + \ ] : [ \ jobstart('sleep 0.010; exit 5', g:dict), \ jobstart('sleep 0.030; exit 5', g:dict), \ jobstart('sleep 0.050; exit 5', g:dict), @@ -507,7 +546,12 @@ describe('jobs', function() it('will return status codes in the order of passed ids', function() source([[ - call rpcnotify(g:channel, 'wait', jobwait([ + call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ + \ jobstart('Start-Sleep -Milliseconds 700; exit 4'), + \ jobstart('Start-Sleep -Milliseconds 500; exit 5'), + \ jobstart('Start-Sleep -Milliseconds 300; exit 6'), + \ jobstart('Start-Sleep -Milliseconds 100; exit 7') + \ ] : [ \ jobstart('sleep 0.070; exit 4'), \ jobstart('sleep 0.050; exit 5'), \ jobstart('sleep 0.030; exit 6'), @@ -521,7 +565,7 @@ describe('jobs', function() source([[ call rpcnotify(g:channel, 'wait', jobwait([ \ -10, - \ jobstart('sleep 0.01; exit 5'), + \ jobstart((has('win32') ? 'Start-Sleep -Milliseconds 100' : 'sleep 0.01').'; exit 5'), \ ])) ]]) eq({'notification', 'wait', {{-3, 5}}}, next_msg()) @@ -530,7 +574,9 @@ describe('jobs', function() it('will return -2 when interrupted without timeout', function() feed_command('call rpcnotify(g:channel, "ready") | '.. 'call rpcnotify(g:channel, "wait", '.. - 'jobwait([jobstart("sleep 10; exit 55")]))') + 'jobwait([jobstart("'.. + (iswin() and 'Start-Sleep 10' or 'sleep 10').. + '; exit 55")]))') eq({'notification', 'ready', {}}, next_msg()) feed('<c-c>') eq({'notification', 'wait', {{-2}}}, next_msg()) @@ -539,7 +585,9 @@ describe('jobs', function() it('will return -2 when interrupted with timeout', function() feed_command('call rpcnotify(g:channel, "ready") | '.. 'call rpcnotify(g:channel, "wait", '.. - 'jobwait([jobstart("sleep 10; exit 55")], 10000))') + 'jobwait([jobstart("'.. + (iswin() and 'Start-Sleep 10' or 'sleep 10').. + '; exit 55")], 10000))') eq({'notification', 'ready', {}}, next_msg()) feed('<c-c>') eq({'notification', 'wait', {{-2}}}, next_msg()) @@ -594,20 +642,22 @@ describe('jobs', function() end) describe('with timeout argument', function() - if helpers.pending_win32(pending) then return end it('will return -1 if the wait timed out', function() source([[ call rpcnotify(g:channel, 'wait', jobwait([ \ jobstart('exit 4'), - \ jobstart('sleep 10; exit 5'), - \ ], 100)) + \ jobstart((has('win32') ? 'Start-Sleep 10' : 'sleep 10').'; exit 5'), + \ ], has('win32') ? 3000 : 100)) ]]) eq({'notification', 'wait', {{4, -1}}}, next_msg()) end) it('can pass 0 to check if a job exists', function() source([[ - call rpcnotify(g:channel, 'wait', jobwait([ + call rpcnotify(g:channel, 'wait', jobwait(has('win32') ? [ + \ jobstart('Start-Sleep -Milliseconds 50; exit 4'), + \ jobstart('Start-Sleep -Milliseconds 300; exit 5'), + \ ] : [ \ jobstart('sleep 0.05; exit 4'), \ jobstart('sleep 0.3; exit 5'), \ ], 0)) @@ -629,13 +679,60 @@ describe('jobs', function() end) it('cannot have both rpc and pty options', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. command("let g:job_opts.pty = v:true") command("let g:job_opts.rpc = v:true") - local _, err = pcall(command, "let j = jobstart(['cat', '-'], g:job_opts)") + local _, err = pcall(command, "let j = jobstart(has('win32') ? ['find', '/v', ''] : ['cat', '-'], g:job_opts)") ok(string.find(err, "E475: Invalid argument: job cannot have both 'pty' and 'rpc' options set") ~= nil) end) + it('does not crash when repeatedly failing to start shell', function() + source([[ + set shell=nosuchshell + func! DoIt() + call jobstart('true') + call jobstart('true') + endfunc + ]]) + -- The crash only triggered if both jobs are cleaned up on the same event + -- loop tick. This is also prevented by try-block, so feed must be used. + feed_command("call DoIt()") + feed('<cr>') -- press RETURN + eq(2,eval('1+1')) + end) + + it('jobstop() kills entire process tree #6530', function() + -- XXX: Using `nvim` isn't a good test, it reaps its children on exit. + -- local c = 'call jobstart([v:progpath, "-u", "NONE", "-i", "NONE", "--headless"])' + -- local j = eval("jobstart([v:progpath, '-u', 'NONE', '-i', 'NONE', '--headless', '-c', '" + -- ..c.."', '-c', '"..c.."'])") + + -- Create child with several descendants. + local sleep_cmd = (iswin() + and 'ping -n 31 127.0.0.1' + or 'sleep 30') + local j = eval("jobstart('"..sleep_cmd..' | '..sleep_cmd..' | '..sleep_cmd.."')") + local ppid = funcs.jobpid(j) + local children + retry(nil, nil, function() + children = meths.get_proc_children(ppid) + eq(3, #children) + end) + -- Assert that nvim_get_proc() sees the children. + for _, child_pid in ipairs(children) do + local info = meths.get_proc(child_pid) + -- eq((iswin() and 'nvim.exe' or 'nvim'), info.name) + eq(ppid, info.ppid) + end + -- Kill the root of the tree. + funcs.jobstop(j) + -- Assert that the children were killed. + retry(nil, nil, function() + for _, child_pid in ipairs(children) do + eq(NIL, meths.get_proc(child_pid)) + end + end) + end) + describe('running tty-test program', function() if helpers.pending_win32(pending) then return end local function next_chunk() diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua new file mode 100644 index 0000000000..cd396ef820 --- /dev/null +++ b/test/functional/core/main_spec.lua @@ -0,0 +1,131 @@ +local lfs = require('lfs') +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local eq = helpers.eq +local feed = helpers.feed +local eval = helpers.eval +local clear = helpers.clear +local funcs = helpers.funcs +local nvim_prog = helpers.nvim_prog +local write_file = helpers.write_file + +local function nvim_prog_abs() + -- system(['build/bin/nvim']) does not work for whatever reason. It needs to + -- either be executable searched in $PATH or something starting with / or ./. + if nvim_prog:match('[/\\]') then + return funcs.fnamemodify(nvim_prog, ':p') + else + return nvim_prog + end +end + +describe('Command-line option', function() + describe('-s', function() + local fname = 'Xtest-functional-core-main-s' + local fname_2 = fname .. '.2' + local nonexistent_fname = fname .. '.nonexistent' + local dollar_fname = '$' .. fname + before_each(function() + clear() + os.remove(fname) + os.remove(dollar_fname) + end) + after_each(function() + os.remove(fname) + os.remove(dollar_fname) + end) + it('treats - as stdin', function() + eq(nil, lfs.attributes(fname)) + funcs.system( + {nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '-s', '-', fname}, + {':call setline(1, "42")', ':wqall!', ''}) + eq(0, eval('v:shell_error')) + local attrs = lfs.attributes(fname) + eq(#('42\n'), attrs.size) + end) + it('does not expand $VAR', function() + eq(nil, lfs.attributes(fname)) + eq(true, not not dollar_fname:find('%$%w+')) + write_file(dollar_fname, ':call setline(1, "100500")\n:wqall!\n') + funcs.system( + {nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '-s', dollar_fname, fname}) + eq(0, eval('v:shell_error')) + local attrs = lfs.attributes(fname) + eq(#('100500\n'), attrs.size) + end) + it('does not crash after reading from stdin in non-headless mode', function() + if helpers.pending_win32(pending) then return end + local screen = Screen.new(40, 8) + screen:attach() + funcs.termopen({ + nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', + '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '-s', '-' + }) + screen:expect([[ + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:[No Name] 0,0-1 All}| + | + | + ]], { + [1] = {foreground = 4210943, special = Screen.colors.Grey0}, + [2] = {special = Screen.colors.Grey0, bold = true, reverse = true} + }) + feed('i:cq<CR>') + screen:expect([[ + | + [Process exited 1] | + | + | + | + | + | + -- TERMINAL -- | + ]]) + --[=[ Example of incorrect output: + screen:expect([[ + ^nvim: /var/tmp/portage/dev-libs/libuv-1.| + 10.2/work/libuv-1.10.2/src/unix/core.c:5| + 19: uv__close: Assertion `fd > STDERR_FI| + LENO' failed. | + | + [Process exited 6] | + | + | + ]]) + ]=] + end) + it('errors out when trying to use nonexistent file with -s', function() + eq( + 'Cannot open for reading: "'..nonexistent_fname..'": no such file or directory\n', + funcs.system( + {nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '--cmd', 'language C', + '-s', nonexistent_fname})) + eq(2, eval('v:shell_error')) + end) + it('errors out when trying to use -s twice', function() + write_file(fname, ':call setline(1, "1")\n:wqall!\n') + write_file(dollar_fname, ':call setline(1, "2")\n:wqall!\n') + eq( + 'Attempt to open script file again: "-s '..dollar_fname..'"\n', + funcs.system( + {nvim_prog_abs(), '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', + '--cmd', 'language C', + '-s', fname, '-s', dollar_fname, fname_2})) + eq(2, eval('v:shell_error')) + eq(nil, lfs.attributes(fname_2)) + end) + end) +end) diff --git a/test/functional/eval/buf_functions_spec.lua b/test/functional/eval/buf_functions_spec.lua index 7de58766b9..c0e264d4c9 100644 --- a/test/functional/eval/buf_functions_spec.lua +++ b/test/functional/eval/buf_functions_spec.lua @@ -15,6 +15,7 @@ local curwinmeths = helpers.curwinmeths local curtabmeths = helpers.curtabmeths local get_pathsep = helpers.get_pathsep local rmdir = helpers.rmdir +local expect_err = helpers.expect_err local fname = 'Xtest-functional-eval-buf_functions' local fname2 = fname .. '.2' @@ -296,8 +297,8 @@ describe('setbufvar() function', function() eq('Vim(call):E461: Illegal variable name: b:', exc_exec('call setbufvar(1, "", 0)')) eq(true, bufmeths.get_var(buf1, 'number')) - funcs.setbufvar(1, 'changedtick', true) - -- eq(true, bufmeths.get_var(buf1, 'changedtick')) + expect_err('Vim:E46: Cannot change read%-only variable "b:changedtick"', + funcs.setbufvar, 1, 'changedtick', true) eq(2, funcs.getbufvar(1, 'changedtick')) end) end) diff --git a/test/functional/eval/execute_spec.lua b/test/functional/eval/execute_spec.lua index 183884a51e..925e311c7d 100644 --- a/test/functional/eval/execute_spec.lua +++ b/test/functional/eval/execute_spec.lua @@ -106,16 +106,22 @@ describe('execute()', function() end) it('does not corrupt the command display #5422', function() - local screen = Screen.new(70, 5) + local screen = Screen.new(70, 7) screen:attach() feed(':echo execute("hi ErrorMsg")<CR>') screen:expect([[ - ~ | - ~ | + | + {1:~ }| + {1:~ }| + {2: }| :echo execute("hi ErrorMsg") | ErrorMsg xxx ctermfg=15 ctermbg=1 guifg=White guibg=Red | - Press ENTER or type command to continue^ | - ]]) + {3:Press ENTER or type command to continue}^ | + ]], { + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {bold = true, reverse = true}, + [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + }) feed('<CR>') end) diff --git a/test/functional/eval/input_spec.lua b/test/functional/eval/input_spec.lua index 1e6b107c60..777f49462d 100644 --- a/test/functional/eval/input_spec.lua +++ b/test/functional/eval/input_spec.lua @@ -58,6 +58,7 @@ before_each(function() RBP2={background=Screen.colors.Yellow}, RBP3={background=Screen.colors.Green}, RBP4={background=Screen.colors.Blue}, + SEP={bold = true, reverse = true}, }) end) @@ -65,9 +66,9 @@ describe('input()', function() it('works with multiline prompts', function() feed([[:call input("Test\nFoo")<CR>]]) screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| Test | Foo^ | ]]) @@ -75,9 +76,9 @@ describe('input()', function() it('works with multiline prompts and :echohl', function() feed([[:echohl Test | call input("Test\nFoo")<CR>]]) screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| {T:Test} | {T:Foo}^ | ]]) @@ -242,17 +243,17 @@ describe('input()', function() it('is not hidden by :silent', function() feed([[:silent call input('Foo: ')<CR>]]) screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| Foo: ^ | | ]]) feed('Bar') screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| Foo: Bar^ | | ]]) @@ -263,9 +264,9 @@ describe('inputdialog()', function() it('works with multiline prompts', function() feed([[:call inputdialog("Test\nFoo")<CR>]]) screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| Test | Foo^ | ]]) @@ -273,9 +274,9 @@ describe('inputdialog()', function() it('works with multiline prompts and :echohl', function() feed([[:echohl Test | call inputdialog("Test\nFoo")<CR>]]) screen:expect([[ + | {EOB:~ }| - {EOB:~ }| - {EOB:~ }| + {SEP: }| {T:Test} | {T:Foo}^ | ]]) diff --git a/test/functional/eval/match_functions_spec.lua b/test/functional/eval/match_functions_spec.lua index 7989b22b5e..0ec465a34c 100644 --- a/test/functional/eval/match_functions_spec.lua +++ b/test/functional/eval/match_functions_spec.lua @@ -6,6 +6,7 @@ local clear = helpers.clear local funcs = helpers.funcs local command = helpers.command local exc_exec = helpers.exc_exec +local expect_err = helpers.expect_err before_each(clear) @@ -40,9 +41,11 @@ describe('setmatches()', function() end) it('fails with -1 if highlight group is not defined', function() - eq(-1, funcs.setmatches({{group=1, pattern=2, id=3, priority=4}})) + expect_err('E28: No such highlight group name: 1', funcs.setmatches, + {{group=1, pattern=2, id=3, priority=4}}) eq({}, funcs.getmatches()) - eq(-1, funcs.setmatches({{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}})) + expect_err('E28: No such highlight group name: 1', funcs.setmatches, + {{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}}) eq({}, funcs.getmatches()) end) end) diff --git a/test/functional/eval/server_spec.lua b/test/functional/eval/server_spec.lua index 393616838e..563e619b39 100644 --- a/test/functional/eval/server_spec.lua +++ b/test/functional/eval/server_spec.lua @@ -1,31 +1,41 @@ - local helpers = require('test.functional.helpers')(after_each) local eq, neq, eval = helpers.eq, helpers.neq, helpers.eval local command = helpers.command local clear, funcs, meths = helpers.clear, helpers.funcs, helpers.meths -local os_name = helpers.os_name +local iswin = helpers.iswin +local ok = helpers.ok +local matches = helpers.matches +local expect_err = helpers.expect_err local function clear_serverlist() - for _, server in pairs(funcs.serverlist()) do - funcs.serverstop(server) - end + for _, server in pairs(funcs.serverlist()) do + funcs.serverstop(server) + end end -describe('serverstart(), serverstop()', function() +describe('server', function() before_each(clear) - it('sets $NVIM_LISTEN_ADDRESS on first invocation', function() + it('serverstart() sets $NVIM_LISTEN_ADDRESS on first invocation', function() -- Unset $NVIM_LISTEN_ADDRESS command('let $NVIM_LISTEN_ADDRESS = ""') local s = eval('serverstart()') assert(s ~= nil and s:len() > 0, "serverstart() returned empty") eq(s, eval('$NVIM_LISTEN_ADDRESS')) - command("call serverstop('"..s.."')") + eq(1, eval("serverstop('"..s.."')")) eq('', eval('$NVIM_LISTEN_ADDRESS')) end) - it('sets v:servername _only_ on nvim startup unless all servers are stopped', + it('sets new v:servername if $NVIM_LISTEN_ADDRESS is invalid', function() + clear({env={NVIM_LISTEN_ADDRESS='.'}}) + eq('.', eval('$NVIM_LISTEN_ADDRESS')) + local servers = funcs.serverlist() + eq(1, #servers) + ok(string.len(servers[1]) > 4) -- Like /tmp/nvim…/… or \\.\pipe\… + end) + + it('sets v:servername at startup or if all servers were stopped', function() local initial_server = meths.get_vvar('servername') assert(initial_server ~= nil and initial_server:len() > 0, @@ -38,24 +48,23 @@ describe('serverstart(), serverstop()', function() neq(initial_server, s) -- serverstop() does _not_ modify v:servername... - funcs.serverstop(s) + eq(1, funcs.serverstop(s)) eq(initial_server, meths.get_vvar('servername')) -- ...unless we stop _all_ servers. - funcs.serverstop(funcs.serverlist()[1]) + eq(1, funcs.serverstop(funcs.serverlist()[1])) eq('', meths.get_vvar('servername')) -- v:servername will take the next available server. - local servername = (os_name() == 'windows' - and [[\\.\pipe\Xtest-functional-server-pipe]] - or 'Xtest-functional-server-socket') + local servername = (iswin() and [[\\.\pipe\Xtest-functional-server-pipe]] + or 'Xtest-functional-server-socket') funcs.serverstart(servername) eq(servername, meths.get_vvar('servername')) end) - it('serverstop() ignores invalid input', function() - command("call serverstop('')") - command("call serverstop('bogus-socket-name')") + it('serverstop() returns false for invalid input', function() + eq(0, eval("serverstop('')")) + eq(0, eval("serverstop('bogus-socket-name')")) end) it('parses endpoints correctly', function() @@ -78,35 +87,32 @@ describe('serverstart(), serverstop()', function() local expected = {} local v4 = '127.0.0.1:12345' - s = funcs.serverstart(v4) - if #s > 0 then + local status, _ = pcall(funcs.serverstart, v4) + if status then table.insert(expected, v4) - funcs.serverstart(v4) -- exists already; ignore + pcall(funcs.serverstart, v4) -- exists already; ignore end local v6 = '::1:12345' - s = funcs.serverstart(v6) - if #s > 0 then + status, _ = pcall(funcs.serverstart, v6) + if status then table.insert(expected, v6) - funcs.serverstart(v6) -- exists already; ignore + pcall(funcs.serverstart, v6) -- exists already; ignore end eq(expected, funcs.serverlist()) clear_serverlist() - funcs.serverstart('127.0.0.1:65536') -- invalid port + expect_err('Failed to start server: invalid argument', + funcs.serverstart, '127.0.0.1:65536') -- invalid port eq({}, funcs.serverlist()) end) -end) - -describe('serverlist()', function() - before_each(clear) - it('returns the list of servers', function() + it('serverlist() returns the list of servers', function() -- There should already be at least one server. local n = eval('len(serverlist())') - -- Add a few - local servs = (os_name() == 'windows' + -- Add some servers. + local servs = (iswin() and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] } or { [[Xtest-pipe0934]], [[Xtest-pipe4324]] }) for _, s in ipairs(servs) do @@ -120,9 +126,31 @@ describe('serverlist()', function() -- The new servers should be at the end of the list. for i = 1, #servs do eq(servs[i], new_servs[i + n]) - command("call serverstop('"..servs[i].."')") + eq(1, eval("serverstop('"..servs[i].."')")) end -- After serverstop() the servers should NOT be in the list. eq(n, eval('len(serverlist())')) end) end) + +describe('startup --listen', function() + it('validates', function() + clear() + + local cmd = { unpack(helpers.nvim_argv) } + table.insert(cmd, '--listen') + matches('nvim.*: Argument missing after: "%-%-listen"', funcs.system(cmd)) + + cmd = { unpack(helpers.nvim_argv) } + table.insert(cmd, '--listen2') + matches('nvim.*: Garbage after option argument: "%-%-listen2"', funcs.system(cmd)) + end) + + it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function() + local addr = (iswin() and [[\\.\pipe\Xtest-listen-pipe]] + or 'Xtest-listen-pipe') + clear({ env={ NVIM_LISTEN_ADDRESS='Xtest-env-pipe' }, + args={ '--listen', addr } }) + eq(addr, meths.get_vvar('servername')) + end) +end) diff --git a/test/functional/eval/sort_spec.lua b/test/functional/eval/sort_spec.lua index 4e5a0afba4..82557575ce 100644 --- a/test/functional/eval/sort_spec.lua +++ b/test/functional/eval/sort_spec.lua @@ -8,6 +8,7 @@ local meths = helpers.meths local funcs = helpers.funcs local command = helpers.command local exc_exec = helpers.exc_exec +local redir_exec = helpers.redir_exec before_each(clear) @@ -38,4 +39,18 @@ describe('sort()', function() eq('[-1.0e-4, function(\'tr\'), v:true, v:false, v:null, [], {\'a\': 42}, \'check\', 1.0e-4]', eval('string(g:list)')) end) + + it('can yield E702 and stop sorting after that', function() + command([[ + function Cmp(a, b) + if type(a:a) == type([]) || type(a:b) == type([]) + return [] + endif + return (a:a > a:b) - (a:a < a:b) + endfunction + ]]) + eq('\nE745: Using a List as a Number\nE702: Sort compare function failed', + redir_exec('let sl = sort([1, 0, [], 3, 2], "Cmp")')) + eq({1, 0, {}, 3, 2}, meths.get_var('sl')) + end) end) diff --git a/test/functional/eval/system_spec.lua b/test/functional/eval/system_spec.lua index 446afefb59..23cea4c038 100644 --- a/test/functional/eval/system_spec.lua +++ b/test/functional/eval/system_spec.lua @@ -120,33 +120,47 @@ describe('system()', function() end end) - describe('executes shell function if passed a string', function() + describe('executes shell function', function() local screen before_each(function() - clear() - screen = Screen.new() - screen:attach() + clear() + screen = Screen.new() + screen:attach() end) after_each(function() - screen:detach() + screen:detach() end) if iswin() then + local function test_more() + eq('root = true', eval([[get(split(system('"more" ".editorconfig"'), "\n"), 0, '')]])) + end + local function test_shell_unquoting() + eval([[system('"ping" "-n" "1" "127.0.0.1"')]]) + eq(0, eval('v:shell_error')) + eq('"a b"\n', eval([[system('cmd /s/c "cmd /s/c "cmd /s/c "echo "a b""""')]])) + eq('"a b"\n', eval([[system('powershell -NoProfile -NoLogo -ExecutionPolicy RemoteSigned -Command echo ''\^"a b\^"''')]])) + end + it('with shell=cmd.exe', function() command('set shell=cmd.exe') eq('""\n', eval([[system('echo ""')]])) eq('"a b"\n', eval([[system('echo "a b"')]])) eq('a \nb\n', eval([[system('echo a & echo b')]])) eq('a \n', eval([[system('echo a 2>&1')]])) + test_more() eval([[system('cd "C:\Program Files"')]]) eq(0, eval('v:shell_error')) + test_shell_unquoting() end) it('with shell=cmd', function() command('set shell=cmd') eq('"a b"\n', eval([[system('echo "a b"')]])) + test_more() + test_shell_unquoting() end) it('with shell=$COMSPEC', function() @@ -154,6 +168,8 @@ describe('system()', function() if comspecshell == 'cmd.exe' then command('set shell=$COMSPEC') eq('"a b"\n', eval([[system('echo "a b"')]])) + test_more() + test_shell_unquoting() else pending('$COMSPEC is not cmd.exe: ' .. comspecshell) end @@ -187,7 +203,7 @@ describe('system()', function() ]]) end) - it('`yes` and is interrupted with CTRL-C', function() + it('`yes` interrupted with CTRL-C', function() feed(':call system("' .. (iswin() and 'for /L %I in (1,0,2) do @echo y' or 'yes') .. '")<cr>') @@ -240,7 +256,7 @@ describe('system()', function() end) it('to backgrounded command does not crash', function() -- This is indeterminate, just exercise the codepath. May get E5677. - feed_command('call system("echo -n echoed &")') + feed_command('call system(has("win32") ? "start /b /wait cmd /c echo echoed" : "echo -n echoed &")') local v_errnum = string.match(eval("v:errmsg"), "^E%d*:") if v_errnum then eq("E5677:", v_errnum) @@ -255,7 +271,7 @@ describe('system()', function() end) it('to backgrounded command does not crash', function() -- This is indeterminate, just exercise the codepath. May get E5677. - feed_command('call system("cat - &", "input")') + feed_command('call system(has("win32") ? "start /b /wait more" : "cat - &", "input")') local v_errnum = string.match(eval("v:errmsg"), "^E%d*:") if v_errnum then eq("E5677:", v_errnum) @@ -299,7 +315,7 @@ describe('system()', function() after_each(delete_file(fname)) it('replaces NULs by SOH characters', function() - eq('part1\001part2\001part3\n', eval('system("cat '..fname..'")')) + eq('part1\001part2\001part3\n', eval([[system('"cat" "]]..fname..[["')]])) end) end) @@ -366,7 +382,7 @@ describe('systemlist()', function() end end) - describe('exectues shell function', function() + describe('executes shell function', function() local screen before_each(function() @@ -399,7 +415,7 @@ describe('systemlist()', function() ]]) end) - it('`yes` and is interrupted with CTRL-C', function() + it('`yes` interrupted with CTRL-C', function() feed(':call systemlist("yes | xargs")<cr>') screen:expect([[ | @@ -464,7 +480,7 @@ describe('systemlist()', function() after_each(delete_file(fname)) it('replaces NULs by newline characters', function() - eq({'part1\npart2\npart3'}, eval('systemlist("cat '..fname..'")')) + eq({'part1\npart2\npart3'}, eval([[systemlist('"cat" "]]..fname..[["')]])) end) end) diff --git a/test/functional/eval/uniq_spec.lua b/test/functional/eval/uniq_spec.lua new file mode 100644 index 0000000000..0e7a013e32 --- /dev/null +++ b/test/functional/eval/uniq_spec.lua @@ -0,0 +1,31 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local clear = helpers.clear +local meths = helpers.meths +local command = helpers.command +local exc_exec = helpers.exc_exec +local redir_exec = helpers.redir_exec + +before_each(clear) + +describe('uniq()', function() + it('errors out when processing special values', function() + eq('Vim(call):E907: Using a special value as a Float', + exc_exec('call uniq([v:true, v:false], "f")')) + end) + + it('can yield E882 and stop filtering after that', function() + command([[ + function Cmp(a, b) + if type(a:a) == type([]) || type(a:b) == type([]) + return [] + endif + return (a:a > a:b) - (a:a < a:b) + endfunction + ]]) + eq('\nE745: Using a List as a Number\nE882: Uniq compare function failed', + redir_exec('let fl = uniq([0, 0, [], 1, 1], "Cmp")')) + eq({0, {}, 1, 1}, meths.get_var('fl')) + end) +end) diff --git a/test/functional/ex_cmds/cmd_map_spec.lua b/test/functional/ex_cmds/cmd_map_spec.lua new file mode 100644 index 0000000000..77d025dcc7 --- /dev/null +++ b/test/functional/ex_cmds/cmd_map_spec.lua @@ -0,0 +1,772 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local feed_command = helpers.feed_command +local feed = helpers.feed +local eq = helpers.eq +local expect = helpers.expect +local eval = helpers.eval +local funcs = helpers.funcs +local insert = helpers.insert +local exc_exec = helpers.exc_exec +local Screen = require('test.functional.ui.screen') + +describe('mappings with <Cmd>', function() + local screen + local function cmdmap(lhs, rhs) + feed_command('noremap '..lhs..' <Cmd>'..rhs..'<cr>') + feed_command('noremap! '..lhs..' <Cmd>'..rhs..'<cr>') + end + + before_each(function() + clear() + screen = Screen.new(65, 8) + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [4] = {bold = true}, + [5] = {background = Screen.colors.LightGrey}, + [6] = {foreground = Screen.colors.Blue1}, + [7] = {bold = true, reverse = true}, + }) + screen:attach() + + cmdmap('<F3>', 'let m = mode(1)') + cmdmap('<F4>', 'normal! ww') + cmdmap('<F5>', 'normal! "ay') + cmdmap('<F6>', 'throw "very error"') + feed_command([[ + function! TextObj() + if mode() !=# "v" + normal! v + end + call cursor(1,3) + normal! o + call cursor(2,4) + endfunction]]) + cmdmap('<F7>', 'call TextObj()') + insert([[ + some short lines + of test text]]) + feed('gg') + cmdmap('<F8>', 'startinsert') + cmdmap('<F9>', 'stopinsert') + feed_command("abbr foo <Cmd>let g:y = 17<cr>bar") + end) + + it('can be displayed', function() + feed_command('map <F3>') + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {6:<F3>} {6:*} {6:<Cmd>}let m = mode(1){6:<CR>} | + ]]) + end) + + it('handles invalid mappings', function() + feed_command('let x = 0') + feed_command('noremap <F3> <Cmd><Cmd>let x = 1<cr>') + feed('<F3>') + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:E5521: <Cmd> mapping must end with <CR> before second <Cmd>} | + ]]) + + feed_command('noremap <F3> <Cmd><F3>let x = 2<cr>') + feed('<F3>') + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:E5522: <Cmd> mapping must not include <F3> key} | + ]]) + + feed_command('noremap <F3> <Cmd>let x = 3') + feed('<F3>') + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:E5520: <Cmd> mapping must end with <CR>} | + ]]) + eq(0, eval('x')) + end) + + it('works in various modes and sees correct `mode()` value', function() + -- normal mode + feed('<F3>') + eq('n', eval('m')) + + -- visual mode + feed('v<F3>') + eq('v', eval('m')) + -- didn't leave visual mode + eq('v', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- visual mapping in select mode + feed('gh<F3>') + eq('v', eval('m')) + -- didn't leave select mode + eq('s', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- select mode mapping + feed_command('snoremap <F3> <Cmd>let m = mode(1)<cr>') + feed('gh<F3>') + eq('s', eval('m')) + -- didn't leave select mode + eq('s', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- operator-pending mode + feed("d<F3>") + eq('no', eval('m')) + -- did leave operator-pending mode + eq('n', eval('mode(1)')) + + --insert mode + feed('i<F3>') + eq('i', eval('m')) + eq('i', eval('mode(1)')) + + -- replace mode + feed("<Ins><F3>") + eq('R', eval('m')) + eq('R', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- virtual replace mode + feed("gR<F3>") + eq('Rv', eval('m')) + eq('Rv', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- langmap works, but is not distinguished in mode(1) + feed(":set iminsert=1<cr>i<F3>") + eq('i', eval('m')) + eq('i', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + feed(':<F3>') + eq('c', eval('m')) + eq('c', eval('mode(1)')) + feed('<esc>') + eq('n', eval('mode(1)')) + + -- terminal mode + feed_command('tnoremap <F3> <Cmd>let m = mode(1)<cr>') + feed_command('split | terminal') + feed('i') + eq('t', eval('mode(1)')) + feed('<F3>') + eq('t', eval('m')) + eq('t', eval('mode(1)')) + end) + + it('works in normal mode', function() + cmdmap('<F2>', 'let s = [mode(1), v:count, v:register]') + + -- check v:count and v:register works + feed('<F2>') + eq({'n', 0, '"'}, eval('s')) + feed('7<F2>') + eq({'n', 7, '"'}, eval('s')) + feed('"e<F2>') + eq({'n', 0, 'e'}, eval('s')) + feed('5"k<F2>') + eq({'n', 5, 'k'}, eval('s')) + feed('"+2<F2>') + eq({'n', 2, '+'}, eval('s')) + + -- text object enters visual mode + feed('<F7>') + screen:expect([[ + so{5:me short lines} | + {5:of }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- VISUAL --} | + ]]) + feed('<esc>') + + -- startinsert + feed('<F8>') + eq('i', eval('mode(1)')) + feed('<esc>') + + eq('n', eval('mode(1)')) + cmdmap(',a', 'call feedkeys("aalpha") \\| let g:a = getline(2)') + cmdmap(',b', 'call feedkeys("abeta", "x") \\| let g:b = getline(2)') + + feed(',a<F3>') + screen:expect([[ + some short lines | + of alpha^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + -- feedkeys were not executed immediately + eq({'n', 'of test text'}, eval('[m,a]')) + eq('i', eval('mode(1)')) + feed('<esc>') + + feed(',b<F3>') + screen:expect([[ + some short lines | + of alphabet^atest text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + -- feedkeys(..., 'x') was executed immediately, but insert mode gets aborted + eq({'n', 'of alphabetatest text'}, eval('[m,b]')) + eq('n', eval('mode(1)')) + end) + + it('works in :normal command', function() + feed_command('noremap ,x <Cmd>call append(1, "xx")\\| call append(1, "aa")<cr>') + feed_command('noremap ,f <Cmd>nosuchcommand<cr>') + feed_command('noremap ,e <Cmd>throw "very error"\\| call append(1, "yy")<cr>') + feed_command('noremap ,m <Cmd>echoerr "The message."\\| call append(1, "zz")<cr>') + feed_command('noremap ,w <Cmd>for i in range(5)\\|if i==1\\|echoerr "Err"\\|endif\\|call append(1, i)\\|endfor<cr>') + + feed(":normal ,x<cr>") + screen:expect([[ + ^some short lines | + aa | + xx | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + + eq('Vim:E492: Not an editor command: nosuchcommand', exc_exec("normal ,f")) + eq('very error', exc_exec("normal ,e")) + eq('Vim(echoerr):The message.', exc_exec("normal ,m")) + feed('w') + screen:expect([[ + some ^short lines | + aa | + xx | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + + feed_command(':%d') + eq('Vim(echoerr):Err', exc_exec("normal ,w")) + screen:expect([[ + ^ | + 0 | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + --No lines in buffer-- | + ]]) + + feed_command(':%d') + feed_command(':normal ,w') + screen:expect([[ + ^ | + 4 | + 3 | + 2 | + 1 | + 0 | + {1:~ }| + {2:Err} | + ]]) + end) + + it('works in visual mode', function() + -- can extend visual mode + feed('v<F4>') + screen:expect([[ + {5:some short }^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- VISUAL --} | + ]]) + eq('v', funcs.mode(1)) + + -- can invoke operator, ending visual mode + feed('<F5>') + eq('n', funcs.mode(1)) + eq({'some short l'}, funcs.getreg('a',1,1)) + + -- error doesn't interrupt visual mode + feed('ggvw<F6>') + screen:expect([[ + {5:some }short lines | + of test text | + {1:~ }| + {1:~ }| + {7: }| + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + {3:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + eq('E605: Exception not caught: very error', eval('v:errmsg')) + -- still in visual mode, <cr> was consumed by the error prompt + screen:expect([[ + {5:some }^short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- VISUAL --} | + ]]) + eq('v', funcs.mode(1)) + feed('<F7>') + screen:expect([[ + so{5:me short lines} | + {5:of }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- VISUAL --} | + ]]) + eq('v', funcs.mode(1)) + + -- startinsert gives "-- (insert) VISUAL --" mode + feed('<F8>') + screen:expect([[ + so{5:me short lines} | + {5:of }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- (insert) VISUAL --} | + ]]) + eq('v', eval('mode(1)')) + feed('<esc>') + eq('i', eval('mode(1)')) + end) + + it('works in select mode', function() + feed_command('snoremap <F1> <cmd>throw "very error"<cr>') + feed_command('snoremap <F2> <cmd>normal! <c-g>"by<cr>') + -- can extend select mode + feed('gh<F4>') + screen:expect([[ + {5:some short }^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- SELECT --} | + ]]) + eq('s', funcs.mode(1)) + + -- visual mapping in select mode restart selct mode after operator + feed('<F5>') + eq('s', funcs.mode(1)) + eq({'some short l'}, funcs.getreg('a',1,1)) + + -- select mode mapping works, and does not restart select mode + feed('<F2>') + eq('n', funcs.mode(1)) + eq({'some short l'}, funcs.getreg('b',1,1)) + + -- error doesn't interrupt temporary visual mode + feed('<esc>ggvw<c-g><F6>') + screen:expect([[ + {5:some }short lines | + of test text | + {1:~ }| + {1:~ }| + {7: }| + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + {3:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + eq('E605: Exception not caught: very error', eval('v:errmsg')) + -- still in visual mode, <cr> was consumed by the error prompt + screen:expect([[ + {5:some }^short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- VISUAL --} | + ]]) + -- quirk: restoration of select mode is not performed + eq('v', funcs.mode(1)) + + -- error doesn't interrupt select mode + feed('<esc>ggvw<c-g><F1>') + screen:expect([[ + {5:some }short lines | + of test text | + {1:~ }| + {1:~ }| + {7: }| + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + {3:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + eq('E605: Exception not caught: very error', eval('v:errmsg')) + -- still in select mode, <cr> was consumed by the error prompt + screen:expect([[ + {5:some }^short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- SELECT --} | + ]]) + -- quirk: restoration of select mode is not performed + eq('s', funcs.mode(1)) + + feed('<F7>') + screen:expect([[ + so{5:me short lines} | + {5:of }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- SELECT --} | + ]]) + eq('s', funcs.mode(1)) + + -- startinsert gives "-- SELECT (insert) --" mode + feed('<F8>') + screen:expect([[ + so{5:me short lines} | + {5:of }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- (insert) SELECT --} | + ]]) + eq('s', eval('mode(1)')) + feed('<esc>') + eq('i', eval('mode(1)')) + end) + + + it('works in operator-pending mode', function() + feed('d<F4>') + expect([[ + lines + of test text]]) + eq({'some short '}, funcs.getreg('"',1,1)) + feed('.') + expect([[ + test text]]) + eq({'lines', 'of '}, funcs.getreg('"',1,1)) + feed('uu') + expect([[ + some short lines + of test text]]) + + -- error aborts operator-pending, operator not performed + feed('d<F6>') + screen:expect([[ + some short lines | + of test text | + {1:~ }| + {1:~ }| + {7: }| + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + {3:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + eq('E605: Exception not caught: very error', eval('v:errmsg')) + expect([[ + some short lines + of test text]]) + + feed('"bd<F7>') + expect([[ + soest text]]) + eq(funcs.getreg('b',1,1), {'me short lines', 'of t'}) + + -- startinsert aborts operator + feed('d<F8>') + eq('i', eval('mode(1)')) + expect([[ + soest text]]) + end) + + it('works in insert mode', function() + + -- works the same as <c-o>w<c-o>w + feed('iindeed <F4>little ') + screen:expect([[ + indeed some short little ^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + + feed('<F6>') + screen:expect([[ + indeed some short little lines | + of test text | + {1:~ }| + {1:~ }| + {7: }| + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + {3:Press ENTER or type command to continue}^ | + ]]) + + + feed('<cr>') + eq('E605: Exception not caught: very error', eval('v:errmsg')) + -- still in insert + screen:expect([[ + indeed some short little ^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + eq('i', eval('mode(1)')) + + -- When entering visual mode from InsertEnter autocmd, an async event, or + -- a <cmd> mapping, vim ends up in undocumented "INSERT VISUAL" mode. If a + -- vim patch decides to disable this mode, this test is expected to fail. + feed('<F7>stuff ') + screen:expect([[ + in{5:deed some short little lines} | + {5:of stuff }^test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT VISUAL --} | + ]]) + expect([[ + indeed some short little lines + of stuff test text]]) + + feed('<F5>') + eq(funcs.getreg('a',1,1), {'deed some short little lines', 'of stuff t'}) + + -- still in insert + screen:expect([[ + in^deed some short little lines | + of stuff test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + eq('i', eval('mode(1)')) + + -- also works as part of abbreviation + feed('<space>foo ') + screen:expect([[ + in bar ^deed some short little lines | + of stuff test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + eq(17, eval('g:y')) + + -- :startinsert does nothing + feed('<F8>') + eq('i', eval('mode(1)')) + + -- :stopinsert works + feed('<F9>') + eq('n', eval('mode(1)')) + end) + + it('works in cmdline mode', function() + cmdmap('<F2>', 'call setcmdpos(2)') + feed(':text<F3>') + eq('c', eval('m')) + -- didn't leave cmdline mode + eq('c', eval('mode(1)')) + feed('<cr>') + eq('n', eval('mode(1)')) + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:E492: Not an editor command: text} | + ]]) + + feed(':echo 2<F6>') + screen:expect([[ + some short lines | + of test text | + {1:~ }| + {7: }| + :echo 2 | + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + :echo 2^ | + ]]) + eq('E605: Exception not caught: very error', eval('v:errmsg')) + -- didn't leave cmdline mode + eq('c', eval('mode(1)')) + feed('+2<cr>') + screen:expect([[ + some short lines | + of test text | + {7: }| + :echo 2 | + {2:Error detected while processing :} | + {2:E605: Exception not caught: very error} | + 4 | + {3:Press ENTER or type command to continue}^ | + ]]) + -- however, message scrolling may cause extra CR prompt + -- This is consistent with output from async events. + feed('<cr>') + screen:expect([[ + ^some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]) + eq('n', eval('mode(1)')) + + feed(':let g:x = 3<F4>') + screen:expect([[ + some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + :let g:x = 3^ | + ]]) + feed('+2<cr>') + -- cursor was moved in the background + screen:expect([[ + some short ^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + :let g:x = 3+2 | + ]]) + eq(5, eval('g:x')) + + feed(':let g:y = 7<F8>') + screen:expect([[ + some short lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + :let g:y = 7^ | + ]]) + eq('c', eval('mode(1)')) + feed('+2<cr>') + -- startinsert takes effect after leaving cmdline mode + screen:expect([[ + some short ^lines | + of test text | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {4:-- INSERT --} | + ]]) + eq('i', eval('mode(1)')) + eq(9, eval('g:y')) + + end) + +end) + diff --git a/test/functional/ex_cmds/digraphs_spec.lua b/test/functional/ex_cmds/digraphs_spec.lua new file mode 100644 index 0000000000..f2d0b76739 --- /dev/null +++ b/test/functional/ex_cmds/digraphs_spec.lua @@ -0,0 +1,38 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local feed = helpers.feed +local Screen = require('test.functional.ui.screen') + +describe(':digraphs', function() + local screen + before_each(function() + clear() + screen = Screen.new(65, 8) + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [4] = {bold = true}, + [5] = {background = Screen.colors.LightGrey}, + [6] = {foreground = Screen.colors.Blue1}, + [7] = {bold = true, reverse = true}, + }) + screen:attach() + end) + + it('displays digraphs', function() + feed(':digraphs<CR>') + screen:expect([[ + A@ {6:Å} 197 E` {6:È} 200 E^ {6:Ê} 202 E" {6:Ë} 203 I` {6:Ì} 204 | + I^ {6:Î} 206 I" {6:Ï} 207 N~ {6:Ñ} 209 O` {6:Ò} 210 O^ {6:Ô} 212 | + O~ {6:Õ} 213 /\ {6:×} 215 U` {6:Ù} 217 U^ {6:Û} 219 Ip {6:Þ} 222 | + a` {6:à} 224 a^ {6:â} 226 a~ {6:ã} 227 a" {6:ä} 228 a@ {6:å} 229 | + e` {6:è} 232 e^ {6:ê} 234 e" {6:ë} 235 i` {6:ì} 236 i^ {6:î} 238 | + n~ {6:ñ} 241 o` {6:ò} 242 o^ {6:ô} 244 o~ {6:õ} 245 u` {6:ù} 249 | + u^ {6:û} 251 y" {6:ÿ} 255 | + {3:Press ENTER or type command to continue}^ | + ]]) + end) + +end) + diff --git a/test/functional/ex_cmds/oldfiles_spec.lua b/test/functional/ex_cmds/oldfiles_spec.lua index 4002855c24..448326cdfb 100644 --- a/test/functional/ex_cmds/oldfiles_spec.lua +++ b/test/functional/ex_cmds/oldfiles_spec.lua @@ -29,6 +29,7 @@ describe(':oldfiles', function() it('shows most recently used files', function() local screen = Screen.new(100, 5) screen:attach() + feed_command("set display-=msgsep") feed_command('edit testfile1') feed_command('edit testfile2') feed_command('wshada') diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index b8d912114d..9895281773 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -12,13 +12,18 @@ local Paths = require('test.config.paths') local check_cores = global_helpers.check_cores local check_logs = global_helpers.check_logs -local neq = global_helpers.neq +local dedent = global_helpers.dedent local eq = global_helpers.eq -local ok = global_helpers.ok -local map = global_helpers.map +local expect_err = global_helpers.expect_err local filter = global_helpers.filter -local dedent = global_helpers.dedent +local map = global_helpers.map +local matches = global_helpers.matches +local neq = global_helpers.neq +local ok = global_helpers.ok +local read_file = global_helpers.read_file +local sleep = global_helpers.sleep local table_flatten = global_helpers.table_flatten +local write_file = global_helpers.write_file local start_dir = lfs.currentdir() -- XXX: NVIM_PROG takes precedence, QuickBuild sets it. @@ -304,12 +309,10 @@ local function retry(max, max_ms, fn) end luv.update_time() -- Update cached value of luv.now() (libuv: uv_now()). if (max and tries >= max) or (luv.now() - start_time > timeout) then - if type(result) == "string" then - result = "\nretry() attempts: "..tostring(tries).."\n"..result - end - error(result) + error("\nretry() attempts: "..tostring(tries).."\n"..tostring(result)) end tries = tries + 1 + luv.sleep(20) -- Avoid hot loop... end end @@ -374,34 +377,6 @@ local function feed_command(...) end end --- Dedent the given text and write it to the file name. -local function write_file(name, text, no_dedent, append) - local file = io.open(name, (append and 'a' or 'w')) - if type(text) == 'table' then - -- Byte blob - local bytes = text - text = '' - for _, char in ipairs(bytes) do - text = ('%s%c'):format(text, char) - end - elseif not no_dedent then - text = dedent(text) - end - file:write(text) - file:flush() - file:close() -end - -local function read_file(name) - local file = io.open(name, 'r') - if not file then - return nil - end - local ret = file:read('*a') - file:close() - return ret -end - local sourced_fnames = {} local function source(code) local fname = tmpname() @@ -425,7 +400,7 @@ end local function set_shell_powershell() source([[ set shell=powershell shellquote=( shellpipe=\| shellredir=> shellxquote= - set shellcmdflag=-NoLogo\ -NoProfile\ -ExecutionPolicy\ RemoteSigned\ -Command + let &shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command Remove-Item -Force alias:sleep;' ]]) end @@ -466,18 +441,6 @@ local function wait() session:request('nvim_eval', '1') end --- sleeps the test runner (_not_ the nvim instance) -local function sleep(ms) - local function notification_cb(method, _) - if method == "redraw" then - error("Screen is attached; use screen:sleep() instead.") - end - return true - end - - run(nil, notification_cb, nil, ms) -end - local function curbuf_contents() wait() -- Before inspecting the buffer, process all input. return table.concat(curbuf('get_lines', 0, -1, true), '\n') @@ -689,31 +652,6 @@ local function alter_slashes(obj) end end -local function hexdump(str) - local len = string.len(str) - local dump = "" - local hex = "" - local asc = "" - - for i = 1, len do - if 1 == i % 8 then - dump = dump .. hex .. asc .. "\n" - hex = string.format("%04x: ", i - 1) - asc = "" - end - - local ord = string.byte(str, i) - hex = hex .. string.format("%02x ", ord) - if ord >= 32 and ord <= 126 then - asc = asc .. string.char(ord) - else - asc = asc .. "." - end - end - - return dump .. hex .. string.rep(" ", 8 - len % 8) .. asc -end - local module = { NIL = mpack.NIL, alter_slashes = alter_slashes, @@ -736,6 +674,7 @@ local module = { exc_exec = exc_exec, expect = expect, expect_any = expect_any, + expect_err = expect_err, expect_msg_seq = expect_msg_seq, expect_twostreams = expect_twostreams, feed = feed, @@ -743,10 +682,10 @@ local module = { filter = filter, funcs = funcs, get_pathsep = get_pathsep, - hexdump = hexdump, insert = insert, iswin = iswin, map = map, + matches = matches, merge_args = merge_args, meth_pcall = meth_pcall, meths = meths, diff --git a/test/functional/insert/insert_spec.lua b/test/functional/insert/insert_spec.lua new file mode 100644 index 0000000000..427954f5a6 --- /dev/null +++ b/test/functional/insert/insert_spec.lua @@ -0,0 +1,41 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert +local command = helpers.command +local eq = helpers.eq +local expect = helpers.expect +local funcs = helpers.funcs + +describe('insert-mode', function() + before_each(function() + clear() + end) + + it('CTRL-@', function() + -- Inserts last-inserted text, leaves insert-mode. + insert('hello') + feed('i<C-@>x') + expect('hellhello') + + -- C-Space is the same as C-@. + -- CTRL-SPC inserts last-inserted text, leaves insert-mode. + feed('i<C-Space>x') + expect('hellhellhello') + + -- CTRL-A inserts last inserted text + feed('i<C-A>x') + expect('hellhellhellhelloxo') + end) + + it('ALT/META #8213', function() + -- Mapped ALT-chord behaves as mapped. + command('inoremap <M-l> meta-l') + command('inoremap <A-j> alt-j') + feed('i<M-l> xxx <A-j><M-h>a<A-h>') + expect('meta-l xxx alt-j') + eq({ 0, 1, 14, 0, }, funcs.getpos('.')) + -- Unmapped ALT-chord behaves as ESC+c. + command('iunmap <M-l>') + feed('0i<M-l>') + eq({ 0, 1, 2, 0, }, funcs.getpos('.')) + end) +end) diff --git a/test/functional/insert/last_inserted_spec.lua b/test/functional/insert/last_inserted_spec.lua deleted file mode 100644 index dce23a3790..0000000000 --- a/test/functional/insert/last_inserted_spec.lua +++ /dev/null @@ -1,22 +0,0 @@ -local helpers = require('test.functional.helpers')(after_each) -local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert -local expect = helpers.expect - -clear() - -describe('insert-mode', function() - it('CTRL-@ inserts last-inserted text, leaves insert-mode', function() - insert('hello') - feed('i<C-@>x') - expect('hellhello') - end) - -- C-Space is the same as C-@ - it('CTRL-SPC inserts last-inserted text, leaves insert-mode', function() - feed('i<C-Space>x') - expect('hellhellhello') - end) - it('CTRL-A inserts last inserted text', function() - feed('i<C-A>x') - expect('hellhellhellhelloxo') - end) -end) diff --git a/test/functional/legacy/077_mf_hash_grow_spec.lua b/test/functional/legacy/077_mf_hash_grow_spec.lua index c692127213..4719a3ecbf 100644 --- a/test/functional/legacy/077_mf_hash_grow_spec.lua +++ b/test/functional/legacy/077_mf_hash_grow_spec.lua @@ -18,7 +18,8 @@ describe('mf_hash_grow()', function() setup(clear) -- Check to see if cksum exists, otherwise skip the test - if os.execute('which cksum 2>&1 > /dev/null') ~= 0 then + local null = helpers.iswin() and 'nul' or '/dev/null' + if os.execute('cksum --help >' .. null .. ' 2>&1') ~= 0 then pending('was not tested because cksum was not found', function() end) else it('is working', function() diff --git a/test/functional/legacy/file_perm_spec.lua b/test/functional/legacy/file_perm_spec.lua index 77e82352c5..d61fdc9b83 100644 --- a/test/functional/legacy/file_perm_spec.lua +++ b/test/functional/legacy/file_perm_spec.lua @@ -15,7 +15,7 @@ describe('Test getting and setting file permissions', function() it('file permissions', function() eq('', call('getfperm', tempfile)) - eq(0, call('setfperm', tempfile, 'r------')) + eq(0, call('setfperm', tempfile, 'r--------')) call('writefile', {'one'}, tempfile) eq(9, call('len', call('getfperm', tempfile))) diff --git a/test/functional/lua/luaeval_spec.lua b/test/functional/lua/luaeval_spec.lua index 6da0001cea..760105df6b 100644 --- a/test/functional/lua/luaeval_spec.lua +++ b/test/functional/lua/luaeval_spec.lua @@ -51,12 +51,12 @@ describe('luaeval()', function() end) describe('recursive lua values', function() it('are successfully transformed', function() - funcs.luaeval('rawset(_G, "d", {})') - funcs.luaeval('rawset(d, "d", d)') + command('lua rawset(_G, "d", {})') + command('lua rawset(d, "d", d)') eq('\n{\'d\': {...@0}}', funcs.execute('echo luaeval("d")')) - funcs.luaeval('rawset(_G, "l", {})') - funcs.luaeval('table.insert(l, l)') + command('lua rawset(_G, "l", {})') + command('lua table.insert(l, l)') eq('\n[[...@0]]', funcs.execute('echo luaeval("l")')) end) end) diff --git a/test/functional/lua/overrides_spec.lua b/test/functional/lua/overrides_spec.lua index 8ca5fe57ba..007d40874f 100644 --- a/test/functional/lua/overrides_spec.lua +++ b/test/functional/lua/overrides_spec.lua @@ -87,6 +87,7 @@ describe('debug.debug', function() E = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, cr = {bold = true, foreground = Screen.colors.SeaGreen4}, }) + command("set display-=msgsep") end) it('works', function() command([[lua diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index 9e29baba2d..f452cafd22 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -5,12 +5,15 @@ local Screen = require('test.functional.ui.screen') local meths = helpers.meths local command = helpers.command local clear = helpers.clear +local exc_exec = helpers.exc_exec local eval = helpers.eval local eq = helpers.eq +local funcs = helpers.funcs local insert = helpers.insert local neq = helpers.neq local mkdir = helpers.mkdir local rmdir = helpers.rmdir +local alter_slashes = helpers.alter_slashes describe('startup defaults', function() describe(':filetype', function() @@ -422,3 +425,276 @@ describe('XDG-based defaults', function() end) end) end) + + +describe('stdpath()', function() + context('returns a String', function() + describe('with "config"' , function () + it('knows XDG_CONFIG_HOME', function() + clear({env={ + XDG_CONFIG_HOME=alter_slashes('/home/docwhat/.config'), + }}) + eq(alter_slashes('/home/docwhat/.config/nvim'), funcs.stdpath('config')) + end) + + it('handles changes during runtime', function() + clear({env={ + XDG_CONFIG_HOME=alter_slashes('/home/original'), + }}) + eq(alter_slashes('/home/original/nvim'), funcs.stdpath('config')) + command("let $XDG_CONFIG_HOME='"..alter_slashes('/home/new').."'") + eq(alter_slashes('/home/new/nvim'), funcs.stdpath('config')) + end) + + it("doesn't expand $VARIABLES", function() + clear({env={ + XDG_CONFIG_HOME='$VARIABLES', + VARIABLES='this-should-not-happen', + }}) + eq(alter_slashes('$VARIABLES/nvim'), funcs.stdpath('config')) + end) + + it("doesn't expand ~/", function() + clear({env={ + XDG_CONFIG_HOME=alter_slashes('~/frobnitz'), + }}) + eq(alter_slashes('~/frobnitz/nvim'), funcs.stdpath('config')) + end) + end) + + describe('with "data"' , function () + local appended_dir + setup(function() + -- Windows appends 'nvim-data' instead of just 'nvim' to + -- prevent collisions due to XDG_CONFIG_HOME and XDG_DATA_HOME + -- being the same. + if helpers.iswin() then + appended_dir = '/nvim-data' + else + appended_dir = '/nvim' + end + end) + + it('knows XDG_DATA_HOME', function() + clear({env={ + XDG_DATA_HOME=alter_slashes('/home/docwhat/.local'), + }}) + eq(alter_slashes('/home/docwhat/.local' .. appended_dir), funcs.stdpath('data')) + end) + + it('handles changes during runtime', function() + clear({env={ + XDG_DATA_HOME=alter_slashes('/home/original'), + }}) + eq(alter_slashes('/home/original' .. appended_dir), funcs.stdpath('data')) + command("let $XDG_DATA_HOME='"..alter_slashes('/home/new').."'") + eq(alter_slashes('/home/new' .. appended_dir), funcs.stdpath('data')) + end) + + it("doesn't expand $VARIABLES", function() + clear({env={ + XDG_DATA_HOME='$VARIABLES', + VARIABLES='this-should-not-happen', + }}) + eq(alter_slashes('$VARIABLES' .. appended_dir), funcs.stdpath('data')) + end) + + it("doesn't expand ~/", function() + clear({env={ + XDG_DATA_HOME=alter_slashes('~/frobnitz'), + }}) + eq(alter_slashes('~/frobnitz' .. appended_dir), funcs.stdpath('data')) + end) + end) + + describe('with "cache"' , function () + it('knows XDG_CACHE_HOME', function() + clear({env={ + XDG_CACHE_HOME=alter_slashes('/home/docwhat/.cache'), + }}) + eq(alter_slashes('/home/docwhat/.cache/nvim'), funcs.stdpath('cache')) + end) + + it('handles changes during runtime', function() + clear({env={ + XDG_CACHE_HOME=alter_slashes('/home/original'), + }}) + eq(alter_slashes('/home/original/nvim'), funcs.stdpath('cache')) + command("let $XDG_CACHE_HOME='"..alter_slashes('/home/new').."'") + eq(alter_slashes('/home/new/nvim'), funcs.stdpath('cache')) + end) + + it("doesn't expand $VARIABLES", function() + clear({env={ + XDG_CACHE_HOME='$VARIABLES', + VARIABLES='this-should-not-happen', + }}) + eq(alter_slashes('$VARIABLES/nvim'), funcs.stdpath('cache')) + end) + + it("doesn't expand ~/", function() + clear({env={ + XDG_CACHE_HOME=alter_slashes('~/frobnitz'), + }}) + eq(alter_slashes('~/frobnitz/nvim'), funcs.stdpath('cache')) + end) + end) + end) + + context('returns a List', function() + -- Some OS specific variables the system would have set. + local function base_env() + if helpers.iswin() then + return { + HOME='C:\\Users\\docwhat', -- technically, is not a usual PATH + HOMEDRIVE='C:', + HOMEPATH='\\Users\\docwhat', + LOCALAPPDATA='C:\\Users\\docwhat\\AppData\\Local', + TEMP='C:\\Users\\docwhat\\AppData\\Local\\Temp', + TMPDIR='C:\\Users\\docwhat\\AppData\\Local\\Temp', + TMP='C:\\Users\\docwhat\\AppData\\Local\\Temp', + } + else + return { + HOME='/home/docwhat', + HOMEDRIVE='HOMEDRIVE-should-be-ignored', + HOMEPATH='HOMEPATH-should-be-ignored', + LOCALAPPDATA='LOCALAPPDATA-should-be-ignored', + TEMP='TEMP-should-be-ignored', + TMPDIR='TMPDIR-should-be-ignored', + TMP='TMP-should-be-ignored', + } + end + end + + local function set_paths_via_system(var_name, paths) + local env = base_env() + env[var_name] = table.concat(paths, ':') + clear({env=env}) + end + + local function set_paths_at_runtime(var_name, paths) + clear({env=base_env()}) + meths.set_var('env_val', table.concat(paths, ':')) + command(('let $%s=g:env_val'):format(var_name)) + end + + local function behaves_like_dir_list_env(msg, stdpath_arg, env_var_name, paths, expected_paths) + describe(msg, function() + it('set via system', function() + set_paths_via_system(env_var_name, paths) + eq(expected_paths, funcs.stdpath(stdpath_arg)) + end) + + it('set at runtime', function() + set_paths_at_runtime(env_var_name, paths) + eq(expected_paths, funcs.stdpath(stdpath_arg)) + end) + end) + end + + describe('with "config_dirs"' , function () + behaves_like_dir_list_env( + 'handles XDG_CONFIG_DIRS with one path', + 'config_dirs', 'XDG_CONFIG_DIRS', + { + alter_slashes('/home/docwhat/.config') + }, + { + alter_slashes('/home/docwhat/.config/nvim') + }) + + behaves_like_dir_list_env( + 'handles XDG_CONFIG_DIRS with two paths', + 'config_dirs', 'XDG_CONFIG_DIRS', + { + alter_slashes('/home/docwhat/.config'), + alter_slashes('/etc/config') + }, + { + alter_slashes('/home/docwhat/.config/nvim'), + alter_slashes('/etc/config/nvim') + }) + + behaves_like_dir_list_env( + "doesn't expand $VAR and $IBLES", + 'config_dirs', 'XDG_CONFIG_DIRS', + { '$HOME', '$TMP' }, + { + alter_slashes('$HOME/nvim'), + alter_slashes('$TMP/nvim') + }) + + + behaves_like_dir_list_env( + "doesn't expand ~/", + 'config_dirs', 'XDG_CONFIG_DIRS', + { + alter_slashes('~/.oldconfig'), + alter_slashes('~/.olderconfig') + }, + { + alter_slashes('~/.oldconfig/nvim'), + alter_slashes('~/.olderconfig/nvim') + }) + end) + + describe('with "data_dirs"' , function () + behaves_like_dir_list_env( + 'knows XDG_DATA_DIRS with one path', + 'data_dirs', 'XDG_DATA_DIRS', + { + alter_slashes('/home/docwhat/.data') + }, + { + alter_slashes('/home/docwhat/.data/nvim') + }) + + behaves_like_dir_list_env( + 'knows XDG_DATA_DIRS with two paths', + 'data_dirs', 'XDG_DATA_DIRS', + { + alter_slashes('/home/docwhat/.data'), + alter_slashes('/etc/local') + }, + { + alter_slashes('/home/docwhat/.data/nvim'), + alter_slashes('/etc/local/nvim'), + }) + + behaves_like_dir_list_env( + "doesn't expand $VAR and $IBLES", + 'data_dirs', 'XDG_DATA_DIRS', + { '$HOME', '$TMP' }, + { + alter_slashes('$HOME/nvim'), + alter_slashes('$TMP/nvim') + }) + + behaves_like_dir_list_env( + "doesn't expand ~/", + 'data_dirs', 'XDG_DATA_DIRS', + { + alter_slashes('~/.oldconfig'), + alter_slashes('~/.olderconfig') + }, + { + alter_slashes('~/.oldconfig/nvim'), + alter_slashes('~/.olderconfig/nvim'), + }) + end) + end) + + describe('errors', function() + it('on unknown strings', function() + eq('Vim(call):E6100: "capybara" is not a valid stdpath', exc_exec('call stdpath("capybara")')) + eq('Vim(call):E6100: "" is not a valid stdpath', exc_exec('call stdpath("")')) + eq('Vim(call):E6100: "23" is not a valid stdpath', exc_exec('call stdpath(23)')) + end) + + it('on non-strings', function() + eq('Vim(call):E731: using Dictionary as a String', exc_exec('call stdpath({"eris": 23})')) + eq('Vim(call):E730: using List as a String', exc_exec('call stdpath([23])')) + end) + end) +end) diff --git a/test/functional/options/num_options_spec.lua b/test/functional/options/num_options_spec.lua new file mode 100644 index 0000000000..ed17ffdd3c --- /dev/null +++ b/test/functional/options/num_options_spec.lua @@ -0,0 +1,97 @@ +-- Tests for :setlocal and :setglobal + +local helpers = require('test.functional.helpers')(after_each) +local clear, feed_command, eval, eq, meths = + helpers.clear, helpers.feed_command, helpers.eval, helpers.eq, helpers.meths + +local function should_fail(opt, value, errmsg) + feed_command('setglobal ' .. opt .. '=' .. value) + eq(errmsg, eval("v:errmsg"):match("E%d*")) + feed_command('let v:errmsg = ""') + feed_command('setlocal ' .. opt .. '=' .. value) + eq(errmsg, eval("v:errmsg"):match("E%d*")) + feed_command('let v:errmsg = ""') + local status, err = pcall(meths.set_option, opt, value) + eq(status, false) + eq(errmsg, err:match("E%d*")) + eq('', eval("v:errmsg")) +end + +local function should_succeed(opt, value) + feed_command('setglobal ' .. opt .. '=' .. value) + feed_command('setlocal ' .. opt .. '=' .. value) + meths.set_option(opt, value) + eq(value, meths.get_option(opt)) + eq('', eval("v:errmsg")) +end + +describe(':setlocal', function() + before_each(clear) + + it('setlocal sets only local value', function() + eq(0, meths.get_option('iminsert')) + feed_command('setlocal iminsert=1') + eq(0, meths.get_option('iminsert')) + eq(0, meths.get_option('imsearch')) + feed_command('setlocal imsearch=1') + eq(0, meths.get_option('imsearch')) + end) +end) + +describe(':set validation', function() + before_each(clear) + + it('setlocal and setglobal validate values', function() + should_fail('shiftwidth', -10, 'E487') + should_succeed('shiftwidth', 0) + should_fail('tabstop', -10, 'E487') + should_fail('winheight', -10, 'E487') + should_fail('winheight', 0, 'E487') + should_fail('winminheight', -1, 'E487') + should_succeed('winminheight', 0) + should_fail('winwidth', 0, 'E487') + should_fail('helpheight', -1, 'E487') + should_fail('maxcombine', 7, 'E474') + should_fail('iminsert', 3, 'E474') + should_fail('imsearch', 3, 'E474') + should_fail('titlelen', -1, 'E487') + should_fail('cmdheight', 0, 'E487') + should_fail('updatecount', -1, 'E487') + should_fail('textwidth', -1, 'E487') + should_fail('tabstop', 0, 'E487') + should_fail('timeoutlen', -1, 'E487') + should_fail('history', 1000000, 'E474') + should_fail('regexpengine', -1, 'E474') + should_fail('regexpengine', 3, 'E474') + should_succeed('regexpengine', 2) + should_fail('report', -1, 'E487') + should_succeed('report', 0) + should_fail('scrolloff', -1, 'E49') + should_fail('sidescrolloff', -1, 'E487') + should_fail('sidescroll', -1, 'E487') + should_fail('cmdwinheight', 0, 'E487') + should_fail('updatetime', -1, 'E487') + + should_fail('foldlevel', -5, 'E487') + should_fail('foldcolumn', 13, 'E474') + should_fail('conceallevel', 4, 'E474') + should_fail('numberwidth', 11, 'E474') + should_fail('numberwidth', 0, 'E487') + + -- If smaller than 1 this one is set to 'lines'-1 + feed_command('setglobal window=-10') + meths.set_option('window', -10) + eq(23, meths.get_option('window')) + eq('', eval("v:errmsg")) + end) + + it('set wmh/wh wmw/wiw checks', function() + feed_command('set winheight=2') + feed_command('set winminheight=3') + eq('E591', eval("v:errmsg"):match("E%d*")) + + feed_command('set winwidth=2') + feed_command('set winminwidth=3') + eq('E592', eval("v:errmsg"):match("E%d*")) + end) +end) diff --git a/test/functional/provider/nodejs_spec.lua b/test/functional/provider/nodejs_spec.lua index 0a12b1a154..f69c3e7c78 100644 --- a/test/functional/provider/nodejs_spec.lua +++ b/test/functional/provider/nodejs_spec.lua @@ -35,7 +35,7 @@ describe('nodejs host', function() nvim.command('call jobstop(g:job_id)'); ]]) command('let g:job_id = jobstart(["node", "'..fname..'"])') - retry(nil, 1000, function() eq('hello', eval('g:job_out')) end) + retry(nil, 2000, function() eq('hello', eval('g:job_out')) end) end) it('plugin works', function() local fname = 'Xtest-nodejs-hello-plugin.js' @@ -45,17 +45,15 @@ describe('nodejs host', function() const nvim = neovim.attach({socket: socket}); class TestPlugin { - hello() { - this.nvim.command('let g:job_out = "hello-plugin"') - } + hello() { + this.nvim.command('let g:job_out = "hello-plugin"'); + } } - const PluginClass = neovim.Plugin(TestPlugin); - const plugin = new PluginClass(nvim); - plugin.hello(); - nvim.command('call jobstop(g:job_id)'); + const plugin = new neovim.NvimPlugin(null, PluginClass, nvim); + plugin.instance.hello(); ]]) command('let g:job_id = jobstart(["node", "'..fname..'"])') - retry(nil, 1000, function() eq('hello-plugin', eval('g:job_out')) end) + retry(nil, 2000, function() eq('hello-plugin', eval('g:job_out')) end) end) end) diff --git a/test/functional/provider/python3_spec.lua b/test/functional/provider/python3_spec.lua index f06728ec0e..93ac3ae017 100644 --- a/test/functional/provider/python3_spec.lua +++ b/test/functional/provider/python3_spec.lua @@ -3,6 +3,7 @@ local eval, command, feed = helpers.eval, helpers.command, helpers.feed local eq, clear, insert = helpers.eq, helpers.clear, helpers.insert local expect, write_file = helpers.expect, helpers.write_file local feed_command = helpers.feed_command +local source = helpers.source local missing_provider = helpers.missing_provider do @@ -13,7 +14,7 @@ do end end -describe('python3 commands and functions', function() +describe('python3 provider', function() before_each(function() clear() command('python3 import vim') @@ -82,4 +83,20 @@ describe('python3 commands and functions', function() it('py3eval', function() eq({1, 2, {['key'] = 'val'}}, eval([[py3eval('[1, 2, {"key": "val"}]')]])) end) + + it('RPC call to expand("<afile>") during BufDelete #5245 #5617', function() + source([=[ + python3 << EOF + import vim + def foo(): + vim.eval('expand("<afile>:p")') + vim.eval('bufnr(expand("<afile>:p"))') + EOF + autocmd BufDelete * python3 foo() + autocmd BufUnload * python3 foo()]=]) + feed_command("exe 'split' tempname()") + feed_command("bwipeout!") + feed_command('help help') + eq(2, eval('1+1')) -- Still alive? + end) end) diff --git a/test/functional/provider/ruby_spec.lua b/test/functional/provider/ruby_spec.lua index a2c6c6a10e..e049ac7c41 100644 --- a/test/functional/provider/ruby_spec.lua +++ b/test/functional/provider/ruby_spec.lua @@ -1,16 +1,18 @@ local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local command = helpers.command +local curbufmeths = helpers.curbufmeths local eq = helpers.eq +local eval = helpers.eval +local expect = helpers.expect local feed = helpers.feed -local clear = helpers.clear +local feed_command = helpers.feed_command local funcs = helpers.funcs -local meths = helpers.meths local insert = helpers.insert -local expect = helpers.expect -local command = helpers.command -local write_file = helpers.write_file -local curbufmeths = helpers.curbufmeths +local meths = helpers.meths local missing_provider = helpers.missing_provider +local write_file = helpers.write_file do clear() @@ -90,3 +92,11 @@ describe(':rubydo command', function() eq(false, curbufmeths.get_option('modified')) end) end) + +describe('ruby provider', function() + it('RPC call to expand("<afile>") during BufDelete #5245 #5617', function() + command([=[autocmd BufDelete * ruby VIM::evaluate('expand("<afile>")')]=]) + feed_command('help help') + eq(2, eval('1+1')) -- Still alive? + end) +end) diff --git a/test/functional/shada/merging_spec.lua b/test/functional/shada/merging_spec.lua index 7a15c8908b..a628baff53 100644 --- a/test/functional/shada/merging_spec.lua +++ b/test/functional/shada/merging_spec.lua @@ -525,6 +525,85 @@ describe('ShaDa marks support code', function() eq('-', funcs.fnamemodify(curbufmeths.get_name(), ':t')) end) + it('can merge with file with mark 9 as the only numeric mark', function() + wshada('\007\001\014\130\161f\196\006' .. mock_file_path .. '-\161n9') + eq(0, exc_exec(sdrcmd())) + nvim_command('normal! `9oabc') + eq('-', funcs.fnamemodify(curbufmeths.get_name(), ':t')) + eq(0, exc_exec('wshada ' .. shada_fname)) + local found = {} + for _, v in ipairs(read_shada_file(shada_fname)) do + if v.type == 7 and v.value.f == mock_file_path .. '-' then + local name = ('%c'):format(v.value.n) + found[name] = (found[name] or 0) + 1 + end + end + eq({['0']=1, ['1']=1}, found) + end) + + it('removes duplicates while merging', function() + wshada('\007\001\014\130\161f\196\006' .. mock_file_path .. '-\161n9' + .. '\007\001\014\130\161f\196\006' .. mock_file_path .. '-\161n9') + eq(0, exc_exec(sdrcmd())) + eq(0, exc_exec('wshada ' .. shada_fname)) + local found = 0 + for _, v in ipairs(read_shada_file(shada_fname)) do + if v.type == 7 and v.value.f == mock_file_path .. '-' then + print(require('test.helpers').format_luav(v)) + found = found + 1 + end + end + eq(1, found) + end) + + it('does not leak when no append is performed due to too many marks', + function() + wshada('\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'a\161n0' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'b\161n1' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'c\161n2' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'd\161n3' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'e\161n4' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'f\161n5' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'g\161n6' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'h\161n7' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'i\161n8' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'j\161n9' + .. '\007\001\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'k\161n9') + eq(0, exc_exec(sdrcmd())) + eq(0, exc_exec('wshada ' .. shada_fname)) + local found = {} + for _, v in ipairs(read_shada_file(shada_fname)) do + if v.type == 7 and v.value.f:sub(1, #mock_file_path) == mock_file_path then + found[#found + 1] = v.value.f:sub(#v.value.f) + end + end + eq({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}, found) + end) + + it('does not leak when last mark in file removes some of the earlier ones', + function() + wshada('\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'a\161n0' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'b\161n1' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'c\161n2' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'd\161n3' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'e\161n4' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'f\161n5' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'g\161n6' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'h\161n7' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'i\161n8' + .. '\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'j\161n9' + .. '\007\003\018\131\162mX\195\161f\196\006' .. mock_file_path .. 'k\161n9') + eq(0, exc_exec(sdrcmd())) + eq(0, exc_exec('wshada ' .. shada_fname)) + local found = {} + for _, v in ipairs(read_shada_file(shada_fname)) do + if v.type == 7 and v.value.f:sub(1, #mock_file_path) == mock_file_path then + found[#found + 1] = v.value.f:sub(#v.value.f) + end + end + eq({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k'}, found) + end) + it('uses last A mark with gt timestamp from file when reading with !', function() wshada('\007\001\018\131\162mX\195\161f\196\006' .. mock_file_path .. '-\161nA') @@ -563,13 +642,14 @@ describe('ShaDa marks support code', function() nvim_command('normal! `A') eq('-', funcs.fnamemodify(curbufmeths.get_name(), ':t')) eq(0, exc_exec('wshada ' .. shada_fname)) - local found = 0 + local found = {} for _, v in ipairs(read_shada_file(shada_fname)) do - if v.type == 7 and v.value.f == '' .. mock_file_path .. '-' then - found = found + 1 + if v.type == 7 and v.value.f == mock_file_path .. '-' then + local name = ('%c'):format(v.value.n) + found[name] = (found[name] or 0) + 1 end end - eq(1, found) + eq({['0']=1, A=1}, found) end) it('uses last A mark with eq timestamp from instance when writing', @@ -580,30 +660,33 @@ describe('ShaDa marks support code', function() nvim_command('normal! `A') eq('-', funcs.fnamemodify(curbufmeths.get_name(), ':t')) eq(0, exc_exec('wshada ' .. shada_fname)) - local found = 0 + local found = {} for _, v in ipairs(read_shada_file(shada_fname)) do if v.type == 7 and v.value.f == mock_file_path .. '-' then - found = found + 1 + local name = ('%c'):format(v.value.n) + found[name] = (found[name] or 0) + 1 end end - eq(1, found) + eq({['0']=1, A=1}, found) end) - it('uses last A mark with gt timestamp from file when writing', - function() + it('uses last A mark with gt timestamp from file when writing', function() wshada('\007\001\018\131\162mX\195\161f\196\006' .. mock_file_path .. '-\161nA') eq(0, exc_exec(sdrcmd())) wshada('\007\002\018\131\162mX\195\161f\196\006' .. mock_file_path .. '?\161nA') nvim_command('normal! `A') eq('-', funcs.fnamemodify(curbufmeths.get_name(), ':t')) eq(0, exc_exec('wshada ' .. shada_fname)) - local found = 0 + local found = {} for _, v in ipairs(read_shada_file(shada_fname)) do - if v.type == 7 and v.value.f == '' .. mock_file_path .. '?' then - found = found + 1 + if v.type == 7 then + local name = ('%c'):format(v.value.n) + local t = found[name] or {} + t[v.value.f] = (t[v.value.f] or 0) + 1 + found[name] = t end end - eq(1, found) + eq({['0']={[mock_file_path .. '-']=1}, A={[mock_file_path .. '?']=1}}, found) end) it('uses last a mark with gt timestamp from instance when reading', diff --git a/test/functional/shada/shada_spec.lua b/test/functional/shada/shada_spec.lua index ca44026852..720855860a 100644 --- a/test/functional/shada/shada_spec.lua +++ b/test/functional/shada/shada_spec.lua @@ -181,13 +181,13 @@ describe('ShaDa support code', function() nvim_command('set shada+=%') nvim_command('wshada! ' .. shada_fname) local readme_fname = funcs.resolve(paths.test_source_path) .. '/README.md' - eq({[7]=1, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) + eq({[7]=2, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) nvim_command('set shada+=r~') nvim_command('wshada! ' .. shada_fname) eq({}, find_file(readme_fname)) nvim_command('set shada-=r~') nvim_command('wshada! ' .. shada_fname) - eq({[7]=1, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) + eq({[7]=2, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) nvim_command('set shada+=r' .. funcs.escape( funcs.escape(paths.test_source_path, '$~'), ' "\\,')) nvim_command('wshada! ' .. shada_fname) @@ -206,7 +206,7 @@ describe('ShaDa support code', function() nvim_command('undo') nvim_command('set shada+=%') nvim_command('wshada! ' .. shada_fname) - eq({[7]=1, [8]=2, [9]=1, [10]=4, [11]=2}, find_file(fname)) + eq({[7]=2, [8]=2, [9]=1, [10]=4, [11]=2}, find_file(fname)) nvim_command('set shada+=r' .. pwd .. '/АБВ') nvim_command('wshada! ' .. shada_fname) eq({}, find_file(fname)) diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua index d2b2d8a60c..84d7ae6e9c 100644 --- a/test/functional/terminal/edit_spec.lua +++ b/test/functional/terminal/edit_spec.lua @@ -36,6 +36,7 @@ describe(':edit term://*', function() local scr = get_screen(columns, lines) local rep = 'a' meths.set_option('shellcmdflag', 'REP ' .. rep) + command('set shellxquote=') -- win: avoid extra quotes local rep_size = rep:byte() -- 'a' => 97 local sb = 10 command('autocmd TermOpen * :setlocal scrollback='..tostring(sb) diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua index 0a30b9bb02..4f22f7385d 100644 --- a/test/functional/terminal/ex_terminal_spec.lua +++ b/test/functional/terminal/ex_terminal_spec.lua @@ -8,6 +8,7 @@ local funcs = helpers.funcs local retry = helpers.retry local ok = helpers.ok local iswin = helpers.iswin +local command = helpers.command describe(':terminal', function() local screen @@ -143,6 +144,7 @@ describe(':terminal (with fake shell)', function() end) it('executes a given command through the shell', function() + command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell('echo hi') screen:expect([[ ^ready $ echo hi | @@ -154,6 +156,7 @@ describe(':terminal (with fake shell)', function() it("executes a given command through the shell, when 'shell' has arguments", function() nvim('set_option', 'shell', nvim_dir..'/shell-test -t jeff') + command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell('echo hi') screen:expect([[ ^jeff $ echo hi | @@ -164,6 +167,7 @@ describe(':terminal (with fake shell)', function() end) it('allows quotes and slashes', function() + command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell([[echo 'hello' \ "world"]]) screen:expect([[ ^ready $ echo 'hello' \ "world" | @@ -217,6 +221,7 @@ describe(':terminal (with fake shell)', function() end) it('works with gf', function() + command('set shellxquote=') -- win: avoid extra quotes terminal_with_fake_shell([[echo "scripts/shadacat.py"]]) screen:expect([[ ^ready $ echo "scripts/shadacat.py" | diff --git a/test/functional/terminal/mouse_spec.lua b/test/functional/terminal/mouse_spec.lua index a21d9f0a56..263a5ce79d 100644 --- a/test/functional/terminal/mouse_spec.lua +++ b/test/functional/terminal/mouse_spec.lua @@ -101,7 +101,7 @@ describe('terminal mouse', function() line28 │line28 | line29 │line29 | line30 │line30 | - rows: 5, cols: 24 │rows: 5, cols: 24 | + rows: 5, cols: 25 │rows: 5, cols: 25 | {2:^ } │{2: } | ========== ========== | :vsp | @@ -111,7 +111,7 @@ describe('terminal mouse', function() {7: 1 }^ │line28 | {4:~ }│line29 | {4:~ }│line30 | - {4:~ }│rows: 5, cols: 24 | + {4:~ }│rows: 5, cols: 25 | {4:~ }│{2: } | ========== ========== | :enew | set number | @@ -121,7 +121,7 @@ describe('terminal mouse', function() {7: 27 }line │line28 | {7: 28 }line │line29 | {7: 29 }line │line30 | - {7: 30 }line │rows: 5, cols: 24 | + {7: 30 }line │rows: 5, cols: 25 | {7: 31 }^ │{2: } | ========== ========== | | @@ -131,7 +131,7 @@ describe('terminal mouse', function() {7: 27 }line │line28 | {7: 28 }line │line29 | {7: 29 }line │line30 | - {7: 30 }line │rows: 5, cols: 24 | + {7: 30 }line │rows: 5, cols: 25 | {7: 31 } │{1: } | ========== ========== | {3:-- TERMINAL --} | @@ -142,7 +142,7 @@ describe('terminal mouse', function() screen:expect([[ {7: 27 }line │line29 | {7: 28 }line │line30 | - {7: 29 }line │rows: 5, cols: 24 | + {7: 29 }line │rows: 5, cols: 25 | {7: 30 }line │mouse enabled | {7: 31 } │{1: } | ========== ========== | @@ -155,7 +155,7 @@ describe('terminal mouse', function() screen:expect([[ {7: 21 }line │line29 | {7: 22 }line │line30 | - {7: 23 }line │rows: 5, cols: 24 | + {7: 23 }line │rows: 5, cols: 25 | {7: 24 }line │mouse enabled | {7: 25 }line │{1: } | ========== ========== | @@ -165,7 +165,7 @@ describe('terminal mouse', function() screen:expect([[ {7: 26 }line │line29 | {7: 27 }line │line30 | - {7: 28 }line │rows: 5, cols: 24 | + {7: 28 }line │rows: 5, cols: 25 | {7: 29 }line │mouse enabled | {7: 30 }line │{1: } | ========== ========== | @@ -178,7 +178,7 @@ describe('terminal mouse', function() screen:expect([[ {7: 27 }line │line29 | {7: 28 }l^ine │line30 | - {7: 29 }line │rows: 5, cols: 24 | + {7: 29 }line │rows: 5, cols: 25 | {7: 30 }line │mouse enabled | {7: 31 } │{2: } | ========== ========== | diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 171745eb57..0ae5802a01 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -115,16 +115,12 @@ describe('tui', function() ]]) end) - it('does not mangle unmapped ALT-key chord', function() - -- Vim represents ALT/META by setting the "high bit" of the modified key; - -- we do _not_. #3982 - -- - -- Example: for input ALT+j: - -- * Vim (Nvim prior to #3982) sets high-bit, inserts "ê". - -- * Nvim (after #3982) inserts "j". - feed_data('i\027j') + it('interprets ESC+key as ALT chord', function() + -- Vim represents ALT/META by setting the "high bit" of the modified key: + -- ALT+j inserts "ê". Nvim does not (#3982). + feed_data('i\022\027j') screen:expect([[ - j{1: } | + <M-j>{1: } | {4:~ }| {4:~ }| {4:~ }| @@ -391,15 +387,7 @@ describe('tui FocusGained/FocusLost', function() -- Exit cmdline-mode. Redraws from timers/events are blocked during -- cmdline-mode, so the buffer won't be updated until we exit cmdline-mode. feed_data('\n') - screen:expect([[ - {1: } | - lost | - gained | - {4:~ }| - {5:[No Name] [+] }| - : | - {3:-- TERMINAL --} | - ]]) + screen:expect('lost'..(' '):rep(46)..'\ngained', nil, nil, nil, true) end) end) diff --git a/test/functional/ui/cmdline_highlight_spec.lua b/test/functional/ui/cmdline_highlight_spec.lua index ffb6a26aef..3c316d1cfa 100644 --- a/test/functional/ui/cmdline_highlight_spec.lua +++ b/test/functional/ui/cmdline_highlight_spec.lua @@ -24,6 +24,7 @@ before_each(function() clear() screen = Screen.new(40, 8) screen:attach() + command("set display-=msgsep") source([[ highlight RBP1 guibg=Red highlight RBP2 guibg=Yellow @@ -737,6 +738,22 @@ describe('Command-line coloring', function() feed('<CR><CR>') eq('', meths.get_var('out')) end) + it('does not crash when callback has caught not-a-editor-command exception', + function() + source([[ + function CaughtExc(cmdline) abort + try + gibberish + catch + " Do nothing + endtry + return [] + endfunction + ]]) + set_color_cb('CaughtExc') + start_prompt('1') + eq(1, meths.eval('1')) + end) end) describe('Ex commands coloring support', function() it('works', function() diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua index f8680678ef..41c290a462 100644 --- a/test/functional/ui/cmdline_spec.lua +++ b/test/functional/ui/cmdline_spec.lua @@ -157,6 +157,27 @@ describe('external cmdline', function() end) end) + it("redraws statusline on entering", function() + command('set laststatus=2') + command('set statusline=%{mode()}') + feed(':') + screen:expect([[ + | + {1:~ }| + {1:~ }| + {3:c^ }| + | + ]], nil, nil, function() + eq({{ + content = { { {}, "" } }, + firstc = ":", + indent = 0, + pos = 0, + prompt = "" + }}, cmdline) + end) + end) + it("works with input()", function() feed(':call input("input", "default")<cr>') screen:expect([[ diff --git a/test/functional/ui/cursor_spec.lua b/test/functional/ui/cursor_spec.lua index b47210a777..812c095add 100644 --- a/test/functional/ui/cursor_spec.lua +++ b/test/functional/ui/cursor_spec.lua @@ -194,8 +194,8 @@ describe('ui/cursor', function() if m.blinkoff then m.blinkoff = 400 end if m.blinkwait then m.blinkwait = 700 end end - if m.hl_id then m.hl_id = 48 end - if m.id_lm then m.id_lm = 49 end + if m.hl_id then m.hl_id = 49 end + if m.id_lm then m.id_lm = 50 end end -- Assert the new expectation. diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index 6f1b31964b..ab3b1c3cac 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -94,6 +94,7 @@ describe('highlight defaults', function() clear() screen = Screen.new() screen:attach() + command("set display-=msgsep") end) after_each(function() @@ -312,7 +313,7 @@ describe('highlight defaults', function() end) end) -describe('guisp (special/undercurl)', function() +describe('highlight', function() local screen before_each(function() @@ -321,7 +322,31 @@ describe('guisp (special/undercurl)', function() screen:attach() end) - it('can be set and is applied like foreground or background', function() + it('cterm=standout gui=standout', function() + screen:detach() + screen = Screen.new(20,5) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {standout = true, bold = true, underline = true, + background = Screen.colors.Gray90, foreground = Screen.colors.Blue1}, + [3] = {standout = true, underline = true, + background = Screen.colors.Gray90} + }) + feed_command('hi CursorLine cterm=standout,underline gui=standout,underline') + feed_command('set cursorline') + feed_command('set listchars=space:.,eol:¬,tab:>-,extends:>,precedes:<,trail:* list') + feed('i\t abcd <cr>\t abcd <cr><esc>k') + screen:expect([[ + {1:>-------.}abcd{1:*¬} | + {2:^>-------.}{3:abcd}{2:*¬}{3: }| + {1:¬} | + {1:~ }| + | + ]]) + end) + + it('guisp (special/undercurl)', function() feed_command('syntax on') feed_command('syn keyword TmpKeyword neovim') feed_command('syn keyword TmpKeyword1 special') @@ -650,6 +675,76 @@ describe("'listchars' highlight", function() end) end) +describe("MsgSeparator highlight and msgsep fillchar", function() + before_each(clear) + it("works", function() + local screen = Screen.new(50,5) + screen:set_default_attr_ids({ + [1] = {bold=true, foreground=Screen.colors.Blue}, + [2] = {bold=true, reverse=true}, + [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [4] = {background = Screen.colors.Cyan, bold = true, reverse = true}, + [5] = {bold = true, background = Screen.colors.Magenta} + }) + screen:attach() + + -- defaults + feed_command("ls") + screen:expect([[ + | + {2: }| + :ls | + 1 %a "[No Name]" line 1 | + {3:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') + + feed_command("set fillchars+=msgsep:-") + feed_command("ls") + screen:expect([[ + | + {2:--------------------------------------------------}| + :ls | + 1 %a "[No Name]" line 1 | + {3:Press ENTER or type command to continue}^ | + ]]) + + -- linked to StatusLine per default + feed_command("hi StatusLine guibg=Cyan") + feed_command("ls") + screen:expect([[ + | + {4:--------------------------------------------------}| + :ls | + 1 %a "[No Name]" line 1 | + {3:Press ENTER or type command to continue}^ | + ]]) + + -- but can be unlinked + feed_command("hi clear MsgSeparator") + feed_command("hi MsgSeparator guibg=Magenta gui=bold") + feed_command("ls") + screen:expect([[ + | + {5:--------------------------------------------------}| + :ls | + 1 %a "[No Name]" line 1 | + {3:Press ENTER or type command to continue}^ | + ]]) + + -- when display doesn't contain msgsep, these options have no effect + feed_command("set display-=msgsep") + feed_command("ls") + screen:expect([[ + {1:~ }| + {1:~ }| + :ls | + 1 %a "[No Name]" line 1 | + {3:Press ENTER or type command to continue}^ | + ]]) + end) +end) + describe("'winhighlight' highlight", function() local screen @@ -683,6 +778,9 @@ describe("'winhighlight' highlight", function() [22] = {bold = true, foreground = Screen.colors.SeaGreen4}, [23] = {background = Screen.colors.LightMagenta}, [24] = {background = Screen.colors.WebGray}, + [25] = {bold = true, foreground = Screen.colors.Green1}, + [26] = {background = Screen.colors.Red}, + [27] = {background = Screen.colors.DarkBlue, bold = true, foreground = Screen.colors.Green1}, }) command("hi Background1 guibg=DarkBlue") command("hi Background2 guibg=DarkGreen") @@ -952,6 +1050,39 @@ describe("'winhighlight' highlight", function() ]]) end) + it("background doesn't override syntax background", function() + command('syntax on') + command('syntax keyword Foobar foobar') + command('syntax keyword Article the') + command('hi Foobar guibg=#FF0000') + command('hi Article guifg=#00FF00 gui=bold') + insert('the foobar was foobar') + screen:expect([[ + {25:the} {26:foobar} was {26:fooba}| + {26:^r} | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + + -- winhl=Normal:Group with background doesn't override syntax background, + -- but does combine with syntax foreground. + command('set winhl=Normal:Background1') + screen:expect([[ + {27:the}{1: }{26:foobar}{1: was }{26:fooba}| + {26:^r}{1: }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + | + ]]) + end) + it('can override NonText, Conceal and EndOfBuffer', function() curbufmeths.set_lines(0,-1,true, {"raa\000"}) command('call matchaddpos("Conceal", [[1,2]], 0, -1, {"conceal": "#"})') diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua index 8b0cb82365..ee1a3240a2 100644 --- a/test/functional/ui/inccommand_spec.lua +++ b/test/functional/ui/inccommand_spec.lua @@ -16,6 +16,8 @@ local retry = helpers.retry local source = helpers.source local wait = helpers.wait local nvim = helpers.nvim +local iswin = helpers.iswin +local sleep = helpers.sleep local default_text = [[ Inc substitution on @@ -63,6 +65,7 @@ local function common_setup(screen, inccommand, text) command("syntax on") command("set nohlsearch") command("hi Substitute guifg=red guibg=yellow") + command("set display-=msgsep") screen:attach() screen:set_default_attr_ids({ [1] = {foreground = Screen.colors.Fuchsia}, @@ -1353,6 +1356,23 @@ describe("inccommand=nosplit", function() :echo 'foo'^ | ]]) end) + + it("does not execute trailing bar-separated commands #7494", function() + feed(':%s/two/three/g|q!') + screen:expect([[ + Inc substitution on | + {12:three} lines | + Inc substitution on | + {12:three} lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :%s/two/three/g|q!^ | + ]]) + eq(eval('v:null'), eval('v:exiting')) + end) end) describe(":substitute, 'inccommand' with a failing expression", function() @@ -1832,7 +1852,7 @@ describe(":substitute", function() clear() end) - it(", inccommand=split, highlights multiline substitutions", function() + it("inccommand=split, highlights multiline substitutions", function() common_setup(screen, "split", multiline_text) feed("gg") @@ -1894,7 +1914,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, highlights multiline substitutions", function() + it("inccommand=nosplit, highlights multiline substitutions", function() common_setup(screen, "nosplit", multiline_text) feed("gg") @@ -1937,7 +1957,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, highlights multiple matches on a line", function() + it("inccommand=split, highlights multiple matches on a line", function() common_setup(screen, "split", multimatch_text) command("set gdefault") feed("gg") @@ -1962,7 +1982,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, highlights multiple matches on a line", function() + it("inccommand=nosplit, highlights multiple matches on a line", function() common_setup(screen, "nosplit", multimatch_text) command("set gdefault") feed("gg") @@ -1987,7 +2007,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, with \\zs", function() + it("inccommand=split, with \\zs", function() common_setup(screen, "split", multiline_text) feed("gg") @@ -2011,7 +2031,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, with \\zs", function() + it("inccommand=nosplit, with \\zs", function() common_setup(screen, "nosplit", multiline_text) feed("gg") @@ -2035,7 +2055,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, substitutions of different length", + it("inccommand=split, substitutions of different length", function() common_setup(screen, "split", "T T123 T2T TTT T090804\nx") @@ -2059,7 +2079,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, substitutions of different length", function() + it("inccommand=nosplit, substitutions of different length", function() common_setup(screen, "nosplit", "T T123 T2T TTT T090804\nx") feed(":%s/T\\([0-9]\\+\\)/\\1\\1/g") @@ -2082,7 +2102,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, contraction of lines", function() + it("inccommand=split, contraction of lines", function() local text = [[ T T123 T T123 T2T TT T23423424 x @@ -2131,7 +2151,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, contraction of lines", function() + it("inccommand=nosplit, contraction of lines", function() local text = [[ T T123 T T123 T2T TT T23423424 x @@ -2161,7 +2181,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, multibyte text", function() + it("inccommand=split, multibyte text", function() common_setup(screen, "split", multibyte_text) feed(":%s/£.*ѫ/X¥¥") screen:expect([[ @@ -2202,7 +2222,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=nosplit, multibyte text", function() + it("inccommand=nosplit, multibyte text", function() common_setup(screen, "nosplit", multibyte_text) feed(":%s/£.*ѫ/X¥¥") screen:expect([[ @@ -2243,7 +2263,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, small cmdwinheight", function() + it("inccommand=split, small cmdwinheight", function() common_setup(screen, "split", long_multiline_text) command("set cmdwinheight=2") @@ -2305,7 +2325,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, large cmdwinheight", function() + it("inccommand=split, large cmdwinheight", function() common_setup(screen, "split", long_multiline_text) command("set cmdwinheight=11") @@ -2367,7 +2387,7 @@ describe(":substitute", function() ]]) end) - it(", inccommand=split, lookaround", function() + it("inccommand=split, lookaround", function() common_setup(screen, "split", "something\neverything\nsomeone") feed([[:%s/\(some\)\@<lt>=thing/one/]]) screen:expect([[ @@ -2451,4 +2471,57 @@ describe(":substitute", function() :%s/some\(thing\)\@!/every/^ | ]]) end) + + it('with inccommand during :terminal activity', function() + command("set cmdwinheight=3") + if iswin() then + feed([[:terminal for /L \%I in (1,1,5000) do @(echo xxx & echo xxx & echo xxx)<cr>]]) + else + feed([[:terminal for i in $(seq 1 5000); do printf 'xxx\nxxx\nxxx\n'; done<cr>]]) + end + command('file term') + command('new') + common_setup(screen, 'split', 'foo bar baz\nbar baz fox\nbar foo baz') + command('wincmd =') + + -- Allow some terminal output. + screen:expect([[ + bar baz fox | + bar foo ba^z | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + xxx | + xxx | + xxx | + xxx | + xxx | + xxx | + {10:term }| + | + ]]) + + feed('gg') + feed(':%s/foo/ZZZ') + sleep(50) -- Allow some terminal activity. + screen:expect([[ + {12:ZZZ} bar baz | + bar baz fox | + bar {12:ZZZ} baz | + {15:~ }| + {15:~ }| + {15:~ }| + {11:[No Name] [+] }| + xxx | + xxx | + {10:term }| + |1| {12:ZZZ} bar baz | + |3| bar {12:ZZZ} baz | + {15:~ }| + {10:[Preview] }| + :%s/foo/ZZZ^ | + ]]) + end) end) diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua index 8e62a37ef1..3dd9a2506e 100644 --- a/test/functional/ui/input_spec.lua +++ b/test/functional/ui/input_spec.lua @@ -1,6 +1,7 @@ local helpers = require('test.functional.helpers')(after_each) local clear, feed_command, nvim = helpers.clear, helpers.feed_command, helpers.nvim local feed, next_msg, eq = helpers.feed, helpers.next_msg, helpers.eq +local command = helpers.command local expect = helpers.expect local write_file = helpers.write_file local Screen = require('test.functional.ui.screen') @@ -127,6 +128,10 @@ describe('input utf sequences that contain CSI/K_SPECIAL', function() end) describe('input non-printable chars', function() + after_each(function() + os.remove('Xtest-overwrite') + end) + it("doesn't crash when echoing them back", function() write_file("Xtest-overwrite", [[foobar]]) clear() @@ -137,6 +142,7 @@ describe('input non-printable chars', function() [3] = {bold = true, foreground = Screen.colors.SeaGreen4} }) screen:attach() + command("set display-=msgsep") feed_command("e Xtest-overwrite") screen:expect([[ diff --git a/test/functional/ui/mouse_spec.lua b/test/functional/ui/mouse_spec.lua index 05d5a6409a..debd324977 100644 --- a/test/functional/ui/mouse_spec.lua +++ b/test/functional/ui/mouse_spec.lua @@ -26,6 +26,7 @@ describe('ui/mouse/input', function() [4] = {reverse = true}, [5] = {bold = true, reverse = true}, }) + command("set display-=msgsep") feed('itesting<cr>mouse<cr>support and selection<esc>') screen:expect([[ testing | diff --git a/test/functional/ui/output_spec.lua b/test/functional/ui/output_spec.lua index 4246020fab..02ca566bd8 100644 --- a/test/functional/ui/output_spec.lua +++ b/test/functional/ui/output_spec.lua @@ -40,10 +40,10 @@ describe("shell command :!", function() -- to avoid triggering a UI flush. child_session.feed_data(":!printf foo; sleep 200\n") screen:expect([[ + | {4:~ }| {4:~ }| - {4:~ }| - {4:~ }| + {5: }| :!printf foo; sleep 200 | foo | {3:-- TERMINAL --} | @@ -99,6 +99,7 @@ describe("shell command :!", function() end local screen = Screen.new(50, 4) screen:attach() + command("set display-=msgsep") -- Print TAB chars. #2958 feed([[:!printf '1\t2\t3'<CR>]]) screen:expect([[ @@ -153,6 +154,7 @@ describe("shell command :!", function() [1] = {bold = true, foreground = Screen.colors.Blue1}, [2] = {foreground = Screen.colors.Blue1}, [3] = {bold = true, foreground = Screen.colors.SeaGreen4}, + [4] = {bold = true, reverse = true}, }) screen:attach() end) @@ -170,10 +172,10 @@ describe("shell command :!", function() or [[:!ls bang_filter_spec ]]) feed([[\l]]) screen:expect([[ + | {1:~ }| {1:~ }| - {1:~ }| - {1:~ }| + {4: }| ]]..result..[[ | f1 | f2 | @@ -187,9 +189,9 @@ describe("shell command :!", function() feed_command('!cat test/functional/fixtures/shell_data.txt') screen.bell = false screen:expect([[ + | {1:~ }| - {1:~ }| - {1:~ }| + {4: }| :!cat test/functional/fixtures/shell_data.txt | {2:^@^A^B^C^D^E^F^H} | {2:^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\^]^^^_} | @@ -213,8 +215,8 @@ describe("shell command :!", function() feed_command(cmd) -- Note: only the first example of split composed char works screen:expect([[ - {1:~ }| - {1:~ }| + | + {4: }| :]]..cmd..[[ | å | ref: å̲ | diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua index bbdf576641..06e4a19354 100644 --- a/test/functional/ui/screen_basic_spec.lua +++ b/test/functional/ui/screen_basic_spec.lua @@ -76,11 +76,26 @@ describe('Screen', function() local function check() eq(true, screen.suspended) end + + command('let g:ev = []') + command('autocmd VimResume * :call add(g:ev, "r")') + command('autocmd VimSuspend * :call add(g:ev, "s")') + + eq(false, screen.suspended) command('suspend') + eq({ 's', 'r' }, eval('g:ev')) + screen:expect(check) screen.suspended = false + feed('<c-z>') + eq({ 's', 'r', 's', 'r' }, eval('g:ev')) + screen:expect(check) + screen.suspended = false + + command('suspend') + eq({ 's', 'r', 's', 'r', 's', 'r' }, eval('g:ev')) end) end) @@ -225,8 +240,8 @@ describe('Screen', function() end) end) - describe('tabnew', function() - it('creates a new buffer', function() + describe('tabs', function() + it('tabnew creates a new buffer', function() command('sp') command('vsp') command('vsp') @@ -284,6 +299,62 @@ describe('Screen', function() | ]]) end) + + it('tabline is redrawn after messages', function() + command('tabnew') + screen:expect([[ + {4: [No Name] }{2: [No Name] }{3: }{4:X}| + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + + feed(':echo "'..string.rep('x\\n', 11)..'"<cr>') + screen:expect([[ + {1: }| + x | + x | + x | + x | + x | + x | + x | + x | + x | + x | + x | + | + {7:Press ENTER or type command to continue}^ | + ]]) + + feed('<cr>') + screen:expect([[ + {4: [No Name] }{2: [No Name] }{3: }{4:X}| + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + end) end) describe('insert mode', function() @@ -355,7 +426,8 @@ describe('Screen', function() ]]) end) - it('execute command with multi-line output', function() + it('execute command with multi-line output without msgsep', function() + command("set display-=msgsep") feed(':ls<cr>') screen:expect([[ {0:~ }| @@ -375,6 +447,28 @@ describe('Screen', function() ]]) feed('<cr>') -- skip the "Press ENTER..." state or tests will hang end) + + it('execute command with multi-line output and with msgsep', function() + command("set display+=msgsep") + feed(':ls<cr>') + screen:expect([[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {1: }| + :ls | + 1 %a "[No Name]" line 1 | + {7:Press ENTER or type command to continue}^ | + ]]) + feed('<cr>') -- skip the "Press ENTER..." state or tests will hang + end) end) describe('scrolling and clearing', function() @@ -573,6 +667,7 @@ describe('Screen', function() command('nnoremap <F1> :echo "TEST"<CR>') feed(':ls<CR>') screen:expect([[ + | {0:~ }| {0:~ }| {0:~ }| @@ -582,8 +677,7 @@ describe('Screen', function() {0:~ }| {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| + {1: }| :ls | 1 %a "[No Name]" line 1 | {7:Press ENTER or type command to continue}^ | @@ -607,22 +701,43 @@ describe('Screen', function() ]]) end) end) -end) -describe('nvim_ui_attach()', function() - before_each(function() - clear() - end) - it('handles very large width/height #2180', function() - local screen = Screen.new(999, 999) - screen:attach() - eq(999, eval('&lines')) - eq(999, eval('&columns')) - end) - it('invalid option returns error', function() - local screen = Screen.new() - local status, rv = pcall(function() screen:attach({foo={'foo'}}) end) - eq(false, status) - eq('No such ui option', rv:match("No such .*")) + -- Regression test for #8357 + it('does not have artifacts after temporary chars in insert mode', function() + command('inoremap jk <esc>') + feed('ifooj') + screen:expect([[ + foo^j | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {2:-- INSERT --} | + ]]) + feed('k') + screen:expect([[ + fo^o | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) end) end) diff --git a/test/functional/ui/searchhl_spec.lua b/test/functional/ui/searchhl_spec.lua index 9f273e8dc9..168080a092 100644 --- a/test/functional/ui/searchhl_spec.lua +++ b/test/functional/ui/searchhl_spec.lua @@ -4,6 +4,8 @@ local clear, feed, insert = helpers.clear, helpers.feed, helpers.insert local feed_command = helpers.feed_command local eq = helpers.eq local eval = helpers.eval +local iswin = helpers.iswin +local sleep = helpers.sleep describe('search highlighting', function() local screen @@ -91,6 +93,38 @@ describe('search highlighting', function() ]]) end) + it('is preserved during :terminal activity', function() + if iswin() then + feed([[:terminal for /L \%I in (1,1,5000) do @(echo xxx & echo xxx & echo xxx)<cr>]]) + else + feed([[:terminal for i in $(seq 1 5000); do printf 'xxx\nxxx\nxxx\n'; done<cr>]]) + end + + feed(':file term<CR>') + feed(':vnew<CR>') + insert([[ + foo bar baz + bar baz foo + bar foo baz + ]]) + feed('/foo') + sleep(50) -- Allow some terminal activity. + screen:expect([[ + {3:foo} bar baz {3:│}xxx | + bar baz {2:foo} {3:│}xxx | + bar {2:foo} baz {3:│}xxx | + {3:│}xxx | + {1:~ }{3:│}xxx | + {5:[No Name] [+] }{3:term }| + /foo^ | + ]], { [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {background = Screen.colors.Yellow}, + [3] = {reverse = true}, + [4] = {foreground = Screen.colors.Red}, + [5] = {bold = true, reverse = true}, + }) + end) + it('works with incsearch', function() feed_command('set hlsearch') feed_command('set incsearch') diff --git a/test/helpers.lua b/test/helpers.lua index 91ceed4df1..0d3fe1316b 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -1,6 +1,44 @@ local assert = require('luassert') +local luv = require('luv') local lfs = require('lfs') +local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote) +local function shell_quote(str) + if string.find(str, quote_me) or str == '' then + return '"' .. str:gsub('[$%%"\\]', '\\%0') .. '"' + else + return str + end +end + +local function argss_to_cmd(...) + local cmd = '' + for i = 1, select('#', ...) do + local arg = select(i, ...) + if type(arg) == 'string' then + cmd = cmd .. ' ' ..shell_quote(arg) + else + for _, subarg in ipairs(arg) do + cmd = cmd .. ' ' .. shell_quote(subarg) + end + end + end + return cmd +end + +local function popen_r(...) + return io.popen(argss_to_cmd(...), 'r') +end + +local function popen_w(...) + return io.popen(argss_to_cmd(...), 'w') +end + +-- sleeps the test runner (_not_ the nvim instance) +local function sleep(ms) + luv.sleep(ms) +end + local check_logs_useless_lines = { ['Warning: noted but unhandled ioctl']=1, ['could cause spurious value errors to appear']=2, @@ -16,6 +54,19 @@ end local function ok(res) return assert.is_true(res) end +local function matches(pat, actual) + if nil ~= string.match(actual, pat) then + return true + end + error(string.format('Pattern does not match.\nPattern:\n%s\nActual:\n%s', pat, actual)) +end +-- Expect an error matching pattern `pat`. +local function expect_err(pat, ...) + local fn = select(1, ...) + local fn_args = {...} + table.remove(fn_args, 1) + assert.error_matches(function() return fn(unpack(fn_args)) end, pat) +end -- initial_path: directory to recurse into -- re: include pattern (string) @@ -108,7 +159,7 @@ local uname = (function() return platform end - local status, f = pcall(io.popen, "uname -s") + local status, f = pcall(popen_r, 'uname', '-s') if status then platform = f:read("*l") f:close() @@ -240,7 +291,7 @@ local function check_cores(app, force) end local function which(exe) - local pipe = io.popen('which ' .. exe, 'r') + local pipe = popen_r('which', exe) local ret = pipe:read('*a') pipe:close() if ret == '' then @@ -250,6 +301,19 @@ local function which(exe) end end +local function repeated_read_cmd(...) + for _ = 1, 10 do + local stream = popen_r(...) + local ret = stream:read('*a') + stream:close() + if ret then + return ret + end + end + print('ERROR: Failed to execute ' .. argss_to_cmd(...) .. ': nil return after 10 attempts') + return nil +end + local function shallowcopy(orig) if type(orig) ~= 'table' then return orig @@ -469,6 +533,8 @@ format_luav = function(v, indent, opts) end elseif type(v) == 'nil' then ret = 'nil' + elseif type(v) == 'boolean' then + ret = (v and 'true' or 'false') else print(type(v)) -- Not implemented yet @@ -552,8 +618,62 @@ local function table_flatten(arr) return result end -return { +local function hexdump(str) + local len = string.len(str) + local dump = "" + local hex = "" + local asc = "" + + for i = 1, len do + if 1 == i % 8 then + dump = dump .. hex .. asc .. "\n" + hex = string.format("%04x: ", i - 1) + asc = "" + end + + local ord = string.byte(str, i) + hex = hex .. string.format("%02x ", ord) + if ord >= 32 and ord <= 126 then + asc = asc .. string.char(ord) + else + asc = asc .. "." + end + end + + return dump .. hex .. string.rep(" ", 8 - len % 8) .. asc +end + +local function read_file(name) + local file = io.open(name, 'r') + if not file then + return nil + end + local ret = file:read('*a') + file:close() + return ret +end + +-- Dedent the given text and write it to the file name. +local function write_file(name, text, no_dedent, append) + local file = io.open(name, (append and 'a' or 'w')) + if type(text) == 'table' then + -- Byte blob + local bytes = text + text = '' + for _, char in ipairs(bytes) do + text = ('%s%c'):format(text, char) + end + elseif not no_dedent then + text = dedent(text) + end + file:write(text) + file:flush() + file:close() +end + +local module = { REMOVE_THIS = REMOVE_THIS, + argss_to_cmd = argss_to_cmd, check_cores = check_cores, check_logs = check_logs, concat_tables = concat_tables, @@ -561,6 +681,7 @@ return { deepcopy = deepcopy, dictdiff = dictdiff, eq = eq, + expect_err = expect_err, filter = filter, fixtbl = fixtbl, fixtbl_rec = fixtbl_rec, @@ -568,15 +689,25 @@ return { format_string = format_string, glob = glob, hasenv = hasenv, + hexdump = hexdump, intchar2lua = intchar2lua, map = map, + matches = matches, mergedicts_copy = mergedicts_copy, neq = neq, ok = ok, + popen_r = popen_r, + popen_w = popen_w, + read_file = read_file, + repeated_read_cmd = repeated_read_cmd, + sleep = sleep, shallowcopy = shallowcopy, table_flatten = table_flatten, tmpname = tmpname, uname = uname, updated = updated, which = which, + write_file = write_file, } + +return module diff --git a/test/includes/pre/sys/stat.h b/test/includes/pre/sys/stat.h index c6cac80913..1cb811d579 100644 --- a/test/includes/pre/sys/stat.h +++ b/test/includes/pre/sys/stat.h @@ -1,4 +1,6 @@ -#define _GNU_SOURCE +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif #include <sys/stat.h> static const mode_t kS_IFMT = S_IFMT; diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index 5cc2be50b1..f8143a0125 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -779,7 +779,7 @@ local function gen_itp(it) end local function cppimport(path) - return cimport(Paths.test_include_path .. '/' .. path) + return cimport(Paths.test_source_path .. '/test/includes/pre/' .. path) end cimport('./src/nvim/types.h', './src/nvim/main.h', './src/nvim/os/time.h') diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua index d9c98e8afa..4d58a8934e 100644 --- a/test/unit/os/fileio_spec.lua +++ b/test/unit/os/fileio_spec.lua @@ -113,7 +113,7 @@ end describe('file_open_fd', function() itp('can use file descriptor returned by os_open for reading', function() local fd = m.os_open(file1, m.kO_RDONLY, 0) - local err, fp = file_open_fd(fd, false) + local err, fp = file_open_fd(fd, m.kFileReadOnly) eq(0, err) eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) eq(0, m.file_close(fp, false)) @@ -121,7 +121,7 @@ describe('file_open_fd', function() itp('can use file descriptor returned by os_open for writing', function() eq(nil, lfs.attributes(filec)) local fd = m.os_open(filec, m.kO_WRONLY + m.kO_CREAT, 384) - local err, fp = file_open_fd(fd, true) + local err, fp = file_open_fd(fd, m.kFileWriteOnly) eq(0, err) eq(4, file_write(fp, 'test')) eq(0, m.file_close(fp, false)) @@ -133,7 +133,7 @@ end) describe('file_open_fd_new', function() itp('can use file descriptor returned by os_open for reading', function() local fd = m.os_open(file1, m.kO_RDONLY, 0) - local err, fp = file_open_fd_new(fd, false) + local err, fp = file_open_fd_new(fd, m.kFileReadOnly) eq(0, err) eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) eq(0, m.file_free(fp, false)) @@ -141,7 +141,7 @@ describe('file_open_fd_new', function() itp('can use file descriptor returned by os_open for writing', function() eq(nil, lfs.attributes(filec)) local fd = m.os_open(filec, m.kO_WRONLY + m.kO_CREAT, 384) - local err, fp = file_open_fd_new(fd, true) + local err, fp = file_open_fd_new(fd, m.kFileWriteOnly) eq(0, err) eq(4, file_write(fp, 'test')) eq(0, m.file_free(fp, false)) diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index bc39507c08..ae6dfe6423 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -199,7 +199,7 @@ describe('fs.c', function() itp('returns the absolute path when given an executable inside $PATH', function() local fullpath = exe('ls') - eq(1, fs.path_is_absolute_path(to_cstr(fullpath))) + eq(1, fs.path_is_absolute(to_cstr(fullpath))) end) itp('returns the absolute path when given an executable relative to the current dir', function() @@ -390,7 +390,7 @@ describe('fs.c', function() buf = ffi.new('char[?]', size + 1, ('\0'):rep(size)) end local eof = ffi.new('bool[?]', 1, {true}) - local ret2 = fs.os_read(fd, eof, buf, size) + local ret2 = fs.os_read(fd, eof, buf, size, false) local ret1 = eof[0] local ret3 = '' if buf ~= nil then @@ -408,7 +408,7 @@ describe('fs.c', function() end local iov = ffi.new('struct iovec[?]', #sizes, bufs) local eof = ffi.new('bool[?]', 1, {true}) - local ret2 = fs.os_readv(fd, eof, iov, #sizes) + local ret2 = fs.os_readv(fd, eof, iov, #sizes, false) local ret1 = eof[0] local ret3 = {} for i = 1,#sizes do @@ -418,7 +418,7 @@ describe('fs.c', function() return ret1, ret2, ret3 end local function os_write(fd, data) - return fs.os_write(fd, data, data and #data or 0) + return fs.os_write(fd, data, data and #data or 0, false) end describe('os_path_exists', function() @@ -491,6 +491,22 @@ describe('fs.c', function() end) end) + describe('os_dup', function() + itp('returns new file descriptor', function() + local dup0 = fs.os_dup(0) + local dup1 = fs.os_dup(1) + local dup2 = fs.os_dup(2) + local tbl = {[0]=true, [1]=true, [2]=true, + [tonumber(dup0)]=true, [tonumber(dup1)]=true, + [tonumber(dup2)]=true} + local i = 0 + for _, _ in pairs(tbl) do + i = i + 1 + end + eq(i, 6) -- All fds must be unique + end) + end) + describe('os_open', function() local new_file = 'test_new_file' local existing_file = 'unit-test-directory/test_existing.file' diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index ed597eaed7..e8ce660ce1 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -261,7 +261,7 @@ describe('path.c', function() end) end) -describe('path_shorten_fname_if_possible', function() +describe('path_try_shorten_fname', function() local cwd = lfs.currentdir() before_each(function() @@ -273,22 +273,22 @@ describe('path_shorten_fname_if_possible', function() lfs.rmdir('ut_directory') end) - describe('path_shorten_fname_if_possible', function() + describe('path_try_shorten_fname', function() itp('returns shortened path if possible', function() lfs.chdir('ut_directory') local full = to_cstr(lfs.currentdir() .. '/subdir/file.txt') - eq('subdir/file.txt', (ffi.string(cimp.path_shorten_fname_if_possible(full)))) + eq('subdir/file.txt', (ffi.string(cimp.path_try_shorten_fname(full)))) end) itp('returns `full_path` if a shorter version is not possible', function() local old = lfs.currentdir() lfs.chdir('ut_directory') local full = old .. '/subdir/file.txt' - eq(full, (ffi.string(cimp.path_shorten_fname_if_possible(to_cstr(full))))) + eq(full, (ffi.string(cimp.path_try_shorten_fname(to_cstr(full))))) end) itp('returns NULL if `full_path` is NULL', function() - eq(NULL, (cimp.path_shorten_fname_if_possible(NULL))) + eq(NULL, (cimp.path_try_shorten_fname(NULL))) end) end) end) @@ -585,22 +585,22 @@ describe('path.c', function() end) end) - describe('path_is_absolute_path', function() - local function path_is_absolute_path(filename) + describe('path_is_absolute', function() + local function path_is_absolute(filename) filename = to_cstr(filename) - return cimp.path_is_absolute_path(filename) + return cimp.path_is_absolute(filename) end itp('returns true if filename starts with a slash', function() - eq(OK, path_is_absolute_path('/some/directory/')) + eq(OK, path_is_absolute('/some/directory/')) end) itp('returns true if filename starts with a tilde', function() - eq(OK, path_is_absolute_path('~/in/my/home~/directory')) + eq(OK, path_is_absolute('~/in/my/home~/directory')) end) itp('returns false if filename starts not with slash nor tilde', function() - eq(FAIL, path_is_absolute_path('not/in/my/home~/directory')) + eq(FAIL, path_is_absolute('not/in/my/home~/directory')) end) end) end) diff --git a/test/unit/preprocess.lua b/test/unit/preprocess.lua index 363358d134..1073855a7d 100644 --- a/test/unit/preprocess.lua +++ b/test/unit/preprocess.lua @@ -2,6 +2,10 @@ -- windows, will probably need quite a bit of adjustment to run there. local ffi = require("ffi") +local global_helpers = require('test.helpers') + +local argss_to_cmd = global_helpers.argss_to_cmd +local repeated_read_cmd = global_helpers.repeated_read_cmd local ccs = {} @@ -22,15 +26,6 @@ table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.7"}, type = "gcc"}) table.insert(ccs, {path = {"/usr/bin/env", "clang"}, type = "clang"}) table.insert(ccs, {path = {"/usr/bin/env", "icc"}, type = "gcc"}) -local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote) -local function shell_quote(str) - if string.find(str, quote_me) or str == '' then - return "'" .. string.gsub(str, "'", [['"'"']]) .. "'" - else - return str - end -end - -- parse Makefile format dependencies into a Lua table local function parse_make_deps(deps) -- remove line breaks and line concatenators @@ -149,16 +144,6 @@ function Gcc:add_to_include_path(...) end end -local function argss_to_cmd(...) - local cmd = '' - for i = 1, select('#', ...) do - for _, arg in ipairs(select(i, ...)) do - cmd = cmd .. ' ' .. shell_quote(arg) - end - end - return cmd -end - -- returns a list of the headers files upon which this file relies function Gcc:dependencies(hdr) local cmd = argss_to_cmd(self.path, {'-M', hdr}) .. ' 2>&1' @@ -172,29 +157,15 @@ function Gcc:dependencies(hdr) end end -local function repeated_call(...) - local cmd = argss_to_cmd(...) - for _ = 1, 10 do - local stream = io.popen(cmd) - local ret = stream:read('*a') - stream:close() - if ret then - return ret - end - end - print('ERROR: preprocess.lua: Failed to execute ' .. cmd .. ': nil return after 10 attempts') - return nil -end - function Gcc:filter_standard_defines(defines) if not self.standard_defines then local pseudoheader_fname = 'tmp_empty_pseudoheader.h' local pseudoheader_file = io.open(pseudoheader_fname, 'w') pseudoheader_file:close() - local standard_defines = repeated_call(self.path, - self.preprocessor_extra_flags, - self.get_defines_extra_flags, - {pseudoheader_fname}) + local standard_defines = repeated_read_cmd(self.path, + self.preprocessor_extra_flags, + self.get_defines_extra_flags, + {pseudoheader_fname}) os.remove(pseudoheader_fname) self.standard_defines = {} for line in standard_defines:gmatch('[^\n]+') do @@ -223,9 +194,9 @@ function Gcc:preprocess(previous_defines, ...) pseudoheader_file:flush() pseudoheader_file:close() - local defines = repeated_call(self.path, self.preprocessor_extra_flags, - self.get_defines_extra_flags, - {pseudoheader_fname}) + local defines = repeated_read_cmd(self.path, self.preprocessor_extra_flags, + self.get_defines_extra_flags, + {pseudoheader_fname}) defines = self:filter_standard_defines(defines) -- lfs = require("lfs") @@ -234,9 +205,10 @@ function Gcc:preprocess(previous_defines, ...) -- io.stderr\write("CWD: #{lfs.currentdir!}\n") -- io.stderr\write("CMD: #{cmd}\n") - local declarations = repeated_call(self.path, self.preprocessor_extra_flags, - self.get_declarations_extra_flags, - {pseudoheader_fname}) + local declarations = repeated_read_cmd(self.path, + self.preprocessor_extra_flags, + self.get_declarations_extra_flags, + {pseudoheader_fname}) os.remove(pseudoheader_fname) diff --git a/test/unit/undo_spec.lua b/test/unit/undo_spec.lua new file mode 100644 index 0000000000..f23110b329 --- /dev/null +++ b/test/unit/undo_spec.lua @@ -0,0 +1,215 @@ +local helpers = require('test.unit.helpers')(after_each) +local itp = helpers.gen_itp(it) +local lfs = require('lfs') +local child_call_once = helpers.child_call_once + +local global_helpers = require('test.helpers') +local sleep = global_helpers.sleep + +local ffi = helpers.ffi +local cimport = helpers.cimport +local to_cstr = helpers.to_cstr +local neq = helpers.neq +local eq = helpers.eq + +cimport('./src/nvim/ex_cmds_defs.h') +cimport('./src/nvim/buffer_defs.h') +local options = cimport('./src/nvim/option_defs.h') +-- TODO: remove: local vim = cimport('./src/nvim/vim.h') +local undo = cimport('./src/nvim/undo.h') +local buffer = cimport('./src/nvim/buffer.h') + +local old_p_udir = nil + +-- Values expected by tests. Set in the setup function and destroyed in teardown +local file_buffer = nil +local buffer_hash = nil + +child_call_once(function() + if old_p_udir == nil then + old_p_udir = options.p_udir -- save the old value of p_udir (undodir) + end + + -- create a new buffer + local c_file = to_cstr('Xtest-unit-undo') + file_buffer = buffer.buflist_new(c_file, c_file, 1, buffer.BLN_LISTED) + file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed + + -- TODO(christopher.waldon.dev@gmail.com): replace the 32 with UNDO_HASH_SIZE + -- requires refactor of UNDO_HASH_SIZE into constant/enum for ffi + -- + -- compute a hash for this undofile + buffer_hash = ffi.new('char_u[32]') + undo.u_compute_hash(buffer_hash) +end) + + +describe('u_write_undo', function() + setup(function() + lfs.mkdir('unit-test-directory') + lfs.chdir('unit-test-directory') + options.p_udir = to_cstr(lfs.currentdir()) -- set p_udir to be the test dir + end) + + teardown(function() + lfs.chdir('..') + local success, err = lfs.rmdir('unit-test-directory') + if not success then + print(err) -- inform tester if directory fails to delete + end + options.p_udir = old_p_udir --restore old p_udir + end) + + -- Lua wrapper for u_write_undo + local function u_write_undo(name, forceit, buf, buf_hash) + if name ~= nil then + name = to_cstr(name) + end + + return undo.u_write_undo(name, forceit, buf, buf_hash) + end + + itp('writes an undo file to undodir given a buffer and hash', function() + u_write_undo(nil, false, file_buffer, buffer_hash) + local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false)) + local undo_file = io.open(correct_name, "r") + + neq(undo_file, nil) + local success, err = os.remove(correct_name) -- delete the file now that we're done with it. + if not success then + print(err) -- inform tester if undofile fails to delete + end + end) + + itp('writes a correctly-named undo file to undodir given a name, buffer, and hash', function() + local correct_name = "undofile.test" + u_write_undo(correct_name, false, file_buffer, buffer_hash) + local undo_file = io.open(correct_name, "r") + + neq(undo_file, nil) + local success, err = os.remove(correct_name) -- delete the file now that we're done with it. + if not success then + print(err) -- inform tester if undofile fails to delete + end + end) + + itp('does not write an undofile when the buffer has no valid undofile name', function() + -- TODO(christopher.waldon.dev@gmail.com): Figure out how to test this. + -- it's hard because u_get_undo_file_name() would need to return null + end) + + itp('writes the undofile with the same permissions as the original file', function() + -- Create Test file and set permissions + local test_file_name = "./test.file" + local test_permission_file = io.open(test_file_name, "w") + test_permission_file:write("testing permissions") + test_permission_file:close() + local test_permissions = lfs.attributes(test_file_name).permissions + + -- Create vim buffer + local c_file = to_cstr(test_file_name) + file_buffer = buffer.buflist_new(c_file, c_file, 1, buffer.BLN_LISTED) + file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed + + u_write_undo(nil, false, file_buffer, buffer_hash) + + -- Find out the correct name of the undofile + local undo_file_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false)) + + -- Find out the permissions of the new file + local permissions = lfs.attributes(undo_file_name).permissions + eq(test_permissions, permissions) + + -- delete the file now that we're done with it. + local success, err = os.remove(test_file_name) + if not success then + print(err) -- inform tester if undofile fails to delete + end + success, err = os.remove(undo_file_name) + if not success then + print(err) -- inform tester if undofile fails to delete + end + end) + + itp('writes an undofile only readable by the user if the buffer is unnamed', function() + local correct_permissions = "rw-------" + local undo_file_name = "test.undo" + + -- Create vim buffer + file_buffer = buffer.buflist_new(nil, nil, 1, buffer.BLN_LISTED) + file_buffer.b_u_numhead = 1 -- Pretend that the buffer has been changed + + u_write_undo(undo_file_name, false, file_buffer, buffer_hash) + + -- Find out the permissions of the new file + local permissions = lfs.attributes(undo_file_name).permissions + eq(correct_permissions, permissions) + + -- delete the file now that we're done with it. + local success, err = os.remove(undo_file_name) + if not success then + print(err) -- inform tester if undofile fails to delete + end + end) + + itp('forces writing undo file for :wundo! command', function() + local file_contents = "testing permissions" + -- Write a text file where the undofile should go + local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false)) + global_helpers.write_file(correct_name, file_contents, true, false) + + -- Call with `forceit`. + u_write_undo(correct_name, true, file_buffer, buffer_hash) + + local undo_file_contents = global_helpers.read_file(correct_name) + + neq(file_contents, undo_file_contents) + local success, deletion_err = os.remove(correct_name) -- delete the file now that we're done with it. + if not success then + print(deletion_err) -- inform tester if undofile fails to delete + end + end) + + itp('overwrites an existing undo file', function() + u_write_undo(nil, false, file_buffer, buffer_hash) + local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false)) + + local file_last_modified = lfs.attributes(correct_name).modification + + sleep(1000) -- Ensure difference in timestamps. + file_buffer.b_u_numhead = 1 -- Mark it as if there are changes + u_write_undo(nil, false, file_buffer, buffer_hash) + + local file_last_modified_2 = lfs.attributes(correct_name).modification + + -- print(file_last_modified, file_last_modified_2) + neq(file_last_modified, file_last_modified_2) + local success, err = os.remove(correct_name) -- delete the file now that we're done with it. + if not success then + print(err) -- inform tester if undofile fails to delete + end + end) + + itp('does not overwrite an existing file that is not an undo file', function() + -- TODO: write test + end) + + itp('does not overwrite an existing file that has the wrong permissions', function() + -- TODO: write test + end) + + itp('does not write an undo file if there is no undo information for the buffer', function() + file_buffer.b_u_numhead = 0 -- Mark it as if there is no undo information + local correct_name = ffi.string(undo.u_get_undo_file_name(file_buffer.b_ffname, false)) + + local existing_file = io.open(correct_name,"r") + if existing_file then + existing_file:close() + os.remove(correct_name) + end + u_write_undo(nil, false, file_buffer, buffer_hash) + local undo_file = io.open(correct_name, "r") + + eq(undo_file, nil) + end) +end) |