diff options
Diffstat (limited to 'test/functional/eval')
35 files changed, 3751 insertions, 319 deletions
diff --git a/test/functional/eval/api_functions_spec.lua b/test/functional/eval/api_functions_spec.lua new file mode 100644 index 0000000000..6f440c7d82 --- /dev/null +++ b/test/functional/eval/api_functions_spec.lua @@ -0,0 +1,148 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local lfs = require('lfs') +local neq, eq, command = helpers.neq, helpers.eq, helpers.command +local clear, curbufmeths = helpers.clear, helpers.curbufmeths +local exc_exec, expect, eval = helpers.exc_exec, helpers.expect, helpers.eval +local insert = helpers.insert + +describe('api functions', function() + before_each(clear) + + it("work", function() + command("call nvim_command('let g:test = 1')") + eq(1, eval("nvim_get_var('test')")) + + local buf = eval("nvim_get_current_buf()") + command("call nvim_buf_set_lines("..buf..", 0, -1, v:true, ['aa', 'bb'])") + expect([[ + aa + bb]]) + + command("call nvim_win_set_cursor(0, [1, 1])") + command("call nvim_input('ax<esc>')") + expect([[ + aax + bb]]) + end) + + it("throw errors for invalid arguments", function() + local err = exc_exec('call nvim_get_current_buf("foo")') + eq('Vim(call):E118: Too many arguments for function: nvim_get_current_buf', err) + + err = exc_exec('call nvim_set_option("hlsearch")') + eq('Vim(call):E119: Not enough arguments for function: nvim_set_option', err) + + err = exc_exec('call nvim_buf_set_lines(1, 0, -1, [], ["list"])') + eq('Vim(call):Wrong type for argument 4, expecting Boolean', err) + + err = exc_exec('call nvim_buf_set_lines(0, 0, -1, v:true, "string")') + eq('Vim(call):Wrong type for argument 5, expecting ArrayOf(String)', err) + + err = exc_exec('call nvim_buf_get_number("0")') + eq('Vim(call):Wrong type for argument 1, expecting Buffer', err) + + err = exc_exec('call nvim_buf_line_count(17)') + eq('Vim(call):Invalid buffer id', err) + end) + + + it("use buffer numbers and windows ids as handles", function() + local screen = Screen.new(40, 8) + screen:attach() + local bnr = eval("bufnr('')") + local bhnd = eval("nvim_get_current_buf()") + local wid = eval("win_getid()") + local whnd = eval("nvim_get_current_win()") + eq(bnr, bhnd) + eq(wid, whnd) + + command("new") -- creates new buffer and new window + local bnr2 = eval("bufnr('')") + local bhnd2 = eval("nvim_get_current_buf()") + local wid2 = eval("win_getid()") + local whnd2 = eval("nvim_get_current_win()") + eq(bnr2, bhnd2) + eq(wid2, whnd2) + neq(bnr, bnr2) + neq(wid, wid2) + -- 0 is synonymous to the current buffer + eq(bnr2, eval("nvim_buf_get_number(0)")) + + command("bn") -- show old buffer in new window + eq(bnr, eval("nvim_get_current_buf()")) + eq(bnr, eval("bufnr('')")) + eq(bnr, eval("nvim_buf_get_number(0)")) + eq(wid2, eval("win_getid()")) + eq(whnd2, eval("nvim_get_current_win()")) + end) + + it("get_lines and set_lines use NL to represent NUL", function() + curbufmeths.set_lines(0, -1, true, {"aa\0", "b\0b"}) + eq({'aa\n', 'b\nb'}, eval("nvim_buf_get_lines(0, 0, -1, 1)")) + + command('call nvim_buf_set_lines(0, 1, 2, v:true, ["xx", "\\nyy"])') + eq({'aa\0', 'xx', '\0yy'}, curbufmeths.get_lines(0, -1, 1)) + end) + + it("that are FUNC_ATTR_NOEVAL cannot be called", function() + -- Deprecated vim_ prefix is not exported. + local err = exc_exec('call vim_get_current_buffer("foo")') + eq('Vim(call):E117: Unknown function: vim_get_current_buffer', err) + + -- Deprecated buffer_ prefix is not exported. + err = exc_exec('call buffer_line_count(0)') + eq('Vim(call):E117: Unknown function: buffer_line_count', err) + + -- Functions deprecated before the api functions became available + -- in vimscript are not exported. + err = exc_exec('call buffer_get_line(0, 1)') + eq('Vim(call):E117: Unknown function: buffer_get_line', err) + + -- some api functions are only useful from a msgpack-rpc channel + err = exc_exec('call nvim_subscribe("fancyevent")') + eq('Vim(call):E117: Unknown function: nvim_subscribe', err) + end) + + it('have metadata accessible with api_info()', function() + local api_keys = eval("sort(keys(api_info()))") + eq({'error_types', 'functions', 'types', + 'ui_events', 'ui_options', 'version'}, api_keys) + end) + + it('are highlighted by vim.vim syntax file', function() + if lfs.attributes("build/runtime/syntax/vim/generated.vim",'uid') == nil then + pending("runtime was not built, skipping test") + return + end + local screen = Screen.new(40, 8) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Brown}, + [2] = {foreground = Screen.colors.DarkCyan}, + [3] = {foreground = Screen.colors.SlateBlue}, + [4] = {foreground = Screen.colors.Fuchsia}, + [5] = {bold = true, foreground = Screen.colors.Blue}, + }) + + command("set ft=vim") + command("let &rtp='build/runtime/,'.&rtp") + command("syntax on") + insert([[ + call bufnr('%') + call nvim_input('typing...') + call not_a_function(42)]]) + + screen:expect([[ + {1:call} {2:bufnr}{3:(}{4:'%'}{3:)} | + {1:call} {2:nvim_input}{3:(}{4:'typing...'}{3:)} | + {1:call} not_a_function{3:(}{4:42}{3:^)} | + {5:~ }| + {5:~ }| + {5:~ }| + {5:~ }| + | + ]]) + screen:detach() + end) +end) diff --git a/test/functional/eval/backtick_expansion_spec.lua b/test/functional/eval/backtick_expansion_spec.lua new file mode 100644 index 0000000000..b1b44cfa8b --- /dev/null +++ b/test/functional/eval/backtick_expansion_spec.lua @@ -0,0 +1,50 @@ +local lfs = require('lfs') +local helpers = require('test.functional.helpers')(after_each) +local clear, command, eval, eq = helpers.clear, helpers.command, helpers.eval, helpers.eq +local write_file = helpers.write_file + +describe("backtick expansion", function() + setup(function() + clear() + lfs.mkdir("test-backticks") + write_file("test-backticks/file1", "test file 1") + write_file("test-backticks/file2", "test file 2") + write_file("test-backticks/file3", "test file 3") + lfs.mkdir("test-backticks/subdir") + write_file("test-backticks/subdir/file4", "test file 4") + -- Long path might cause "Press ENTER" prompt; use :silent to avoid it. + command('silent cd test-backticks') + end) + + teardown(function() + helpers.rmdir('test-backticks') + end) + + it("with default 'shell'", function() + if helpers.iswin() then + command(":silent args `dir /b *2`") + else + command(":silent args `echo ***2`") + end + eq({ "file2", }, eval("argv()")) + if helpers.iswin() then + command(":silent args `dir /s/b *4`") + eq({ "subdir\\file4", }, eval("map(argv(), 'fnamemodify(v:val, \":.\")')")) + else + command(":silent args `echo */*4`") + eq({ "subdir/file4", }, eval("argv()")) + end + end) + + it("with shell=fish", function() + if eval("executable('fish')") == 0 then + pending('missing "fish" command') + return + end + command("set shell=fish") + command(":silent args `echo ***2`") + eq({ "file2", }, eval("argv()")) + command(":silent args `echo */*4`") + eq({ "subdir/file4", }, eval("argv()")) + end) +end) diff --git a/test/functional/eval/buf_functions_spec.lua b/test/functional/eval/buf_functions_spec.lua new file mode 100644 index 0000000000..c0e264d4c9 --- /dev/null +++ b/test/functional/eval/buf_functions_spec.lua @@ -0,0 +1,304 @@ +local helpers = require('test.functional.helpers')(after_each) + +local lfs = require('lfs') + +local eq = helpers.eq +local clear = helpers.clear +local funcs = helpers.funcs +local meths = helpers.meths +local command = helpers.command +local exc_exec = helpers.exc_exec +local bufmeths = helpers.bufmeths +local winmeths = helpers.winmeths +local curbufmeths = helpers.curbufmeths +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' +local dirname = fname .. '.d' + +before_each(clear) + +for _, func in ipairs({'bufname(%s)', 'bufnr(%s)', 'bufwinnr(%s)', + 'getbufline(%s, 1)', 'getbufvar(%s, "changedtick")', + 'setbufvar(%s, "f", 0)'}) do + local funcname = func:match('%w+') + describe(funcname .. '() function', function() + it('errors out when receives v:true/v:false/v:null', function() + -- Not compatible with Vim: in Vim it always results in buffer not found + -- without any error messages. + for _, var in ipairs({'v:true', 'v:false', 'v:null'}) do + eq('Vim(call):E5300: Expected a Number or a String', + exc_exec('call ' .. func:format(var))) + end + end) + it('errors out when receives invalid argument', function() + eq('Vim(call):E745: Expected a Number or a String, List found', + exc_exec('call ' .. func:format('[]'))) + eq('Vim(call):E728: Expected a Number or a String, Dictionary found', + exc_exec('call ' .. func:format('{}'))) + eq('Vim(call):E805: Expected a Number or a String, Float found', + exc_exec('call ' .. func:format('0.0'))) + eq('Vim(call):E703: Expected a Number or a String, Funcref found', + exc_exec('call ' .. func:format('function("tr")'))) + end) + end) +end + +describe('bufname() function', function() + it('returns empty string when buffer was not found', function() + command('file ' .. fname) + eq('', funcs.bufname(2)) + eq('', funcs.bufname('non-existent-buffer')) + eq('', funcs.bufname('#')) + command('edit ' .. fname2) + eq(2, funcs.bufnr('%')) + eq('', funcs.bufname('X')) + end) + before_each(function() + lfs.mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected buffer name', function() + eq('', funcs.bufname('%')) -- Buffer has no name yet + command('file ' .. fname) + local wd = lfs.currentdir() + local sep = get_pathsep() + local curdirname = funcs.fnamemodify(wd, ':t') + for _, arg in ipairs({'%', 1, 'X', wd}) do + eq(fname, funcs.bufname(arg)) + meths.set_current_dir('..') + eq(curdirname .. sep .. fname, funcs.bufname(arg)) + meths.set_current_dir(curdirname) + meths.set_current_dir(dirname) + eq(wd .. sep .. fname, funcs.bufname(arg)) + meths.set_current_dir('..') + eq(fname, funcs.bufname(arg)) + command('enew') + end + eq('', funcs.bufname('%')) + eq('', funcs.bufname('$')) + eq(2, funcs.bufnr('%')) + end) +end) + +describe('bufnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, funcs.bufnr(2)) + eq(-1, funcs.bufnr('non-existent-buffer')) + eq(-1, funcs.bufnr('#')) + command('edit ' .. fname2) + eq(2, funcs.bufnr('%')) + eq(-1, funcs.bufnr('X')) + end) + it('returns expected buffer number', function() + eq(1, funcs.bufnr('%')) + command('file ' .. fname) + local wd = lfs.currentdir() + local curdirname = funcs.fnamemodify(wd, ':t') + eq(1, funcs.bufnr(fname)) + eq(1, funcs.bufnr(wd)) + eq(1, funcs.bufnr(curdirname)) + eq(1, funcs.bufnr('X')) + end) + it('returns number of last buffer with "$"', function() + eq(1, funcs.bufnr('$')) + command('new') + eq(2, funcs.bufnr('$')) + command('new') + eq(3, funcs.bufnr('$')) + command('only') + eq(3, funcs.bufnr('$')) + eq(3, funcs.bufnr('%')) + command('buffer 1') + eq(3, funcs.bufnr('$')) + eq(1, funcs.bufnr('%')) + command('bwipeout 2') + eq(3, funcs.bufnr('$')) + eq(1, funcs.bufnr('%')) + command('bwipeout 3') + eq(1, funcs.bufnr('$')) + eq(1, funcs.bufnr('%')) + command('new') + eq(4, funcs.bufnr('$')) + end) +end) + +describe('bufwinnr() function', function() + it('returns -1 when buffer was not found', function() + command('file ' .. fname) + eq(-1, funcs.bufwinnr(2)) + eq(-1, funcs.bufwinnr('non-existent-buffer')) + eq(-1, funcs.bufwinnr('#')) + command('split ' .. fname2) -- It would be OK if there was one window + eq(2, funcs.bufnr('%')) + eq(-1, funcs.bufwinnr('X')) + end) + before_each(function() + lfs.mkdir(dirname) + end) + after_each(function() + rmdir(dirname) + end) + it('returns expected window number', function() + eq(1, funcs.bufwinnr('%')) + command('file ' .. fname) + command('vsplit') + command('split ' .. fname2) + eq(2, funcs.bufwinnr(fname)) + eq(1, funcs.bufwinnr(fname2)) + eq(-1, funcs.bufwinnr(fname:sub(1, #fname - 1))) + meths.set_current_dir(dirname) + eq(2, funcs.bufwinnr(fname)) + eq(1, funcs.bufwinnr(fname2)) + eq(-1, funcs.bufwinnr(fname:sub(1, #fname - 1))) + eq(1, funcs.bufwinnr('%')) + eq(2, funcs.bufwinnr(1)) + eq(1, funcs.bufwinnr(2)) + eq(-1, funcs.bufwinnr(3)) + eq(1, funcs.bufwinnr('$')) + end) +end) + +describe('getbufline() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq({}, funcs.getbufline(2, 1)) + eq({}, funcs.getbufline('non-existent-buffer', 1)) + eq({}, funcs.getbufline('#', 1)) + command('edit ' .. fname2) + eq(2, funcs.bufnr('%')) + eq({}, funcs.getbufline('X', 1)) + end) + it('returns empty list when range is invalid', function() + eq({}, funcs.getbufline(1, 0)) + curbufmeths.set_lines(0, 1, false, {'foo', 'bar', 'baz'}) + eq({}, funcs.getbufline(1, 2, 1)) + eq({}, funcs.getbufline(1, -10, -20)) + eq({}, funcs.getbufline(1, -2, -1)) + eq({}, funcs.getbufline(1, -1, 9999)) + end) + it('returns expected lines', function() + meths.set_option('hidden', true) + command('file ' .. fname) + curbufmeths.set_lines(0, 1, false, {'foo\0', '\0bar', 'baz'}) + command('edit ' .. fname2) + curbufmeths.set_lines(0, 1, false, {'abc\0', '\0def', 'ghi'}) + eq({'foo\n', '\nbar', 'baz'}, funcs.getbufline(1, 1, 9999)) + eq({'abc\n', '\ndef', 'ghi'}, funcs.getbufline(2, 1, 9999)) + eq({'foo\n', '\nbar', 'baz'}, funcs.getbufline(1, 1, '$')) + eq({'baz'}, funcs.getbufline(1, '$', '$')) + eq({'baz'}, funcs.getbufline(1, '$', 9999)) + end) +end) + +describe('getbufvar() function', function() + it('returns empty list when buffer was not found', function() + command('file ' .. fname) + eq('', funcs.getbufvar(2, '&autoindent')) + eq('', funcs.getbufvar('non-existent-buffer', '&autoindent')) + eq('', funcs.getbufvar('#', '&autoindent')) + command('edit ' .. fname2) + eq(2, funcs.bufnr('%')) + eq('', funcs.getbufvar('X', '&autoindent')) + end) + it('returns empty list when variable/option/etc was not found', function() + command('file ' .. fname) + eq('', funcs.getbufvar(1, '&autondent')) + eq('', funcs.getbufvar(1, 'changedtic')) + end) + it('returns expected option value', function() + eq(0, funcs.getbufvar(1, '&autoindent')) + eq(0, funcs.getbufvar(1, '&l:autoindent')) + eq(0, funcs.getbufvar(1, '&g:autoindent')) + -- Also works with global-only options + eq(0, funcs.getbufvar(1, '&hidden')) + eq(0, funcs.getbufvar(1, '&l:hidden')) + eq(0, funcs.getbufvar(1, '&g:hidden')) + -- Also works with window-local options + eq(0, funcs.getbufvar(1, '&number')) + eq(0, funcs.getbufvar(1, '&l:number')) + eq(0, funcs.getbufvar(1, '&g:number')) + command('new') + -- But with window-local options it probably does not what you expect + curwinmeths.set_option('number', true) + -- (note that current window’s buffer is 2, but getbufvar() receives 1) + eq(2, bufmeths.get_number(curwinmeths.get_buf())) + eq(1, funcs.getbufvar(1, '&number')) + eq(1, funcs.getbufvar(1, '&l:number')) + -- You can get global value though, if you find this useful. + eq(0, funcs.getbufvar(1, '&g:number')) + end) + it('returns expected variable value', function() + eq(2, funcs.getbufvar(1, 'changedtick')) + curbufmeths.set_lines(0, 1, false, {'abc\0', '\0def', 'ghi'}) + eq(3, funcs.getbufvar(1, 'changedtick')) + curbufmeths.set_var('test', true) + eq(true, funcs.getbufvar(1, 'test')) + eq({test=true, changedtick=3}, funcs.getbufvar(1, '')) + command('new') + eq(3, funcs.getbufvar(1, 'changedtick')) + eq(true, funcs.getbufvar(1, 'test')) + eq({test=true, changedtick=3}, funcs.getbufvar(1, '')) + end) +end) + +describe('setbufvar() function', function() + it('throws the error or ignores the input when buffer was not found', function() + command('file ' .. fname) + eq(0, + exc_exec('call setbufvar(2, "&autoindent", 0)')) + eq('Vim(call):E94: No matching buffer for non-existent-buffer', + exc_exec('call setbufvar("non-existent-buffer", "&autoindent", 0)')) + eq(0, + exc_exec('call setbufvar("#", "&autoindent", 0)')) + command('edit ' .. fname2) + eq(2, funcs.bufnr('%')) + eq('Vim(call):E93: More than one match for X', + exc_exec('call setbufvar("X", "&autoindent", 0)')) + end) + it('may set options, including window-local and global values', function() + local buf1 = meths.get_current_buf() + eq(false, curwinmeths.get_option('number')) + command('split') + command('new') + eq(2, bufmeths.get_number(curwinmeths.get_buf())) + funcs.setbufvar(1, '&number', true) + local windows = curtabmeths.list_wins() + eq(false, winmeths.get_option(windows[1], 'number')) + eq(true, winmeths.get_option(windows[2], 'number')) + eq(false, winmeths.get_option(windows[3], 'number')) + eq(false, winmeths.get_option(meths.get_current_win(), 'number')) + + eq(false, meths.get_option('hidden')) + funcs.setbufvar(1, '&hidden', true) + eq(true, meths.get_option('hidden')) + + eq(false, bufmeths.get_option(buf1, 'autoindent')) + funcs.setbufvar(1, '&autoindent', true) + eq(true, bufmeths.get_option(buf1, 'autoindent')) + eq('Vim(call):E355: Unknown option: xxx', + exc_exec('call setbufvar(1, "&xxx", 0)')) + end) + it('may set variables', function() + local buf1 = meths.get_current_buf() + command('split') + command('new') + eq(2, curbufmeths.get_number()) + funcs.setbufvar(1, 'number', true) + eq(true, bufmeths.get_var(buf1, 'number')) + eq('Vim(call):E461: Illegal variable name: b:', + exc_exec('call setbufvar(1, "", 0)')) + eq(true, bufmeths.get_var(buf1, 'number')) + 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/capture_spec.lua b/test/functional/eval/capture_spec.lua deleted file mode 100644 index d9265f1b5b..0000000000 --- a/test/functional/eval/capture_spec.lua +++ /dev/null @@ -1,86 +0,0 @@ -local helpers = require('test.functional.helpers')(after_each) -local eq = helpers.eq -local eval = helpers.eval -local clear = helpers.clear -local source = helpers.source -local redir_exec = helpers.redir_exec -local exc_exec = helpers.exc_exec -local funcs = helpers.funcs -local Screen = require('test.functional.ui.screen') -local feed = helpers.feed - -describe('capture()', function() - before_each(clear) - - it('returns the same result with :redir', function() - eq(redir_exec('messages'), funcs.capture('messages')) - end) - - it('returns the output of the commands if the argument is List', function() - eq("foobar", funcs.capture({'echon "foo"', 'echon "bar"'})) - eq("\nfoo\nbar", funcs.capture({'echo "foo"', 'echo "bar"'})) - end) - - it('supports the nested redirection', function() - source([[ - function! g:Foo() - let a = '' - redir => a - silent echon "foo" - redir END - return a - endfunction - function! g:Bar() - let a = '' - redir => a - call g:Foo() - redir END - return a - endfunction - ]]) - eq('foo', funcs.capture('call g:Bar()')) - - eq('42', funcs.capture([[echon capture("echon capture('echon 42')")]])) - end) - - it('returns the transformed string', function() - eq('^A', funcs.capture('echon "\\<C-a>"')) - end) - - it('returns the empty string if the argument list is empty', function() - eq('', funcs.capture({})) - eq(0, exc_exec('let g:ret = capture(v:_null_list)')) - eq('', eval('g:ret')) - end) - - it('returns the errors', function() - local ret - ret = exc_exec('call capture(0.0)') - eq('Vim(call):E806: using Float as a String', ret) - ret = exc_exec('call capture(v:_null_dict)') - eq('Vim(call):E731: using Dictionary as a String', ret) - ret = exc_exec('call capture(function("tr"))') - eq('Vim(call):E729: using Funcref as a String', ret) - ret = exc_exec('call capture(["echo 42", 0.0, "echo 44"])') - eq('Vim(call):E806: using Float as a String', ret) - ret = exc_exec('call capture(["echo 42", v:_null_dict, "echo 44"])') - eq('Vim(call):E731: using Dictionary as a String', ret) - ret = exc_exec('call capture(["echo 42", function("tr"), "echo 44"])') - eq('Vim(call):E729: using Funcref as a String', ret) - end) - - it('silences command run inside', function() - local screen = Screen.new(20, 5) - screen:attach() - screen:set_default_attr_ignore({{bold=true, foreground=255}}) - feed(':let g:mes = capture("echon 42")<CR>') - screen:expect([[ - ^ | - ~ | - ~ | - ~ | - | - ]]) - eq('42', eval('g:mes')) - end) -end) diff --git a/test/functional/eval/changedtick_spec.lua b/test/functional/eval/changedtick_spec.lua new file mode 100644 index 0000000000..60ea9fa12b --- /dev/null +++ b/test/functional/eval/changedtick_spec.lua @@ -0,0 +1,142 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local eval = helpers.eval +local feed = helpers.feed +local clear = helpers.clear +local funcs = helpers.funcs +local meths = helpers.meths +local command = helpers.command +local exc_exec = helpers.exc_exec +local redir_exec = helpers.redir_exec +local meth_pcall = helpers.meth_pcall +local curbufmeths = helpers.curbufmeths + +before_each(clear) + +local function changedtick() + local ct = curbufmeths.get_changedtick() + eq(ct, curbufmeths.get_var('changedtick')) + eq(ct, curbufmeths.get_var('changedtick')) + eq(ct, eval('b:changedtick')) + eq(ct, eval('b:["changedtick"]')) + eq(ct, eval('b:.changedtick')) + eq(ct, funcs.getbufvar('%', 'changedtick')) + eq(ct, funcs.getbufvar('%', '').changedtick) + eq(ct, eval('b:').changedtick) + return ct +end + +describe('b:changedtick', function() + -- Ported tests from Vim-8.0.333 + it('increments', function() -- Test_changedtick_increments + -- New buffer has an empty line, tick starts at 2 + eq(2, changedtick()) + funcs.setline(1, 'hello') + eq(3, changedtick()) + eq(0, exc_exec('undo')) + -- Somehow undo counts as two changes + eq(5, changedtick()) + end) + it('is present in b: dictionary', function() + eq(2, changedtick()) + command('let d = b:') + eq(2, meths.get_var('d').changedtick) + end) + it('increments at bdel', function() + command('new') + eq(2, changedtick()) + local bnr = curbufmeths.get_number() + eq(2, bnr) + command('bdel') + eq(3, funcs.getbufvar(bnr, 'changedtick')) + eq(1, curbufmeths.get_number()) + end) + it('fails to be changed by user', function() + local ct = changedtick() + local ctn = ct + 100500 + eq(0, exc_exec('let d = b:')) + eq('\nE46: Cannot change read-only variable "b:changedtick"', + redir_exec('let b:changedtick = ' .. ctn)) + eq('\nE46: Cannot change read-only variable "b:["changedtick"]"', + redir_exec('let b:["changedtick"] = ' .. ctn)) + eq('\nE46: Cannot change read-only variable "b:.changedtick"', + redir_exec('let b:.changedtick = ' .. ctn)) + eq('\nE46: Cannot change read-only variable "d.changedtick"', + redir_exec('let d.changedtick = ' .. ctn)) + eq({false, 'Key is read-only: changedtick'}, + meth_pcall(curbufmeths.set_var, 'changedtick', ctn)) + + eq('\nE795: Cannot delete variable b:changedtick', + redir_exec('unlet b:changedtick')) + eq('\nE46: Cannot change read-only variable "b:.changedtick"', + redir_exec('unlet b:.changedtick')) + eq('\nE46: Cannot change read-only variable "b:["changedtick"]"', + redir_exec('unlet b:["changedtick"]')) + eq('\nE46: Cannot change read-only variable "d.changedtick"', + redir_exec('unlet d.changedtick')) + eq({false, 'Key is read-only: changedtick'}, + meth_pcall(curbufmeths.del_var, 'changedtick')) + eq(ct, changedtick()) + + eq('\nE46: Cannot change read-only variable "b:["changedtick"]"', + redir_exec('let b:["changedtick"] += ' .. ctn)) + eq('\nE46: Cannot change read-only variable "b:["changedtick"]"', + redir_exec('let b:["changedtick"] -= ' .. ctn)) + eq('\nE46: Cannot change read-only variable "b:["changedtick"]"', + redir_exec('let b:["changedtick"] .= ' .. ctn)) + + eq(ct, changedtick()) + + funcs.setline(1, 'hello') + + eq(ct + 1, changedtick()) + end) + it('is listed in :let output', function() + eq('\nb:changedtick #2', + redir_exec(':let b:')) + end) + it('fails to unlock b:changedtick', function() + eq(0, exc_exec('let d = b:')) + eq(0, funcs.islocked('b:changedtick')) + eq(0, funcs.islocked('d.changedtick')) + eq('\nE940: Cannot lock or unlock variable b:changedtick', + redir_exec('unlockvar b:changedtick')) + eq('\nE46: Cannot change read-only variable "d.changedtick"', + redir_exec('unlockvar d.changedtick')) + eq(0, funcs.islocked('b:changedtick')) + eq(0, funcs.islocked('d.changedtick')) + eq('\nE940: Cannot lock or unlock variable b:changedtick', + redir_exec('lockvar b:changedtick')) + eq('\nE46: Cannot change read-only variable "d.changedtick"', + redir_exec('lockvar d.changedtick')) + eq(0, funcs.islocked('b:changedtick')) + eq(0, funcs.islocked('d.changedtick')) + end) + it('is being completed', function() + feed(':echo b:<Tab><Home>let cmdline="<End>"<CR>') + eq('echo b:changedtick', meths.get_var('cmdline')) + end) + it('cannot be changed by filter() or map()', function() + eq(2, changedtick()) + eq('\nE795: Cannot delete variable filter() argument', + redir_exec('call filter(b:, 0)')) + eq('\nE742: Cannot change value of map() argument', + redir_exec('call map(b:, 0)')) + eq('\nE742: Cannot change value of map() argument', + redir_exec('call map(b:, "v:val")')) + eq(2, changedtick()) + end) + it('cannot be remove()d', function() + eq(2, changedtick()) + eq('\nE795: Cannot delete variable remove() argument', + redir_exec('call remove(b:, "changedtick")')) + eq(2, changedtick()) + end) + it('does not inherit VAR_FIXED when copying dictionary over', function() + eq(2, changedtick()) + eq('', redir_exec('let d1 = copy(b:)|let d1.changedtick = 42')) + eq('', redir_exec('let d2 = copy(b:)|unlet d2.changedtick')) + eq(2, changedtick()) + end) +end) diff --git a/test/functional/eval/container_functions_spec.lua b/test/functional/eval/container_functions_spec.lua new file mode 100644 index 0000000000..04a3248c49 --- /dev/null +++ b/test/functional/eval/container_functions_spec.lua @@ -0,0 +1,24 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local eval = helpers.eval +local meths = helpers.meths +local clear = helpers.clear + +before_each(clear) + +describe('extend()', function() + it('suceeds to extend list with itself', function() + meths.set_var('l', {1, {}}) + eq({1, {}, 1, {}}, eval('extend(l, l)')) + eq({1, {}, 1, {}}, meths.get_var('l')) + + meths.set_var('l', {1, {}}) + eq({1, {}, 1, {}}, eval('extend(l, l, 0)')) + eq({1, {}, 1, {}}, meths.get_var('l')) + + meths.set_var('l', {1, {}}) + eq({1, 1, {}, {}}, eval('extend(l, l, 1)')) + eq({1, 1, {}, {}}, meths.get_var('l')) + end) +end) diff --git a/test/functional/eval/executable_spec.lua b/test/functional/eval/executable_spec.lua new file mode 100644 index 0000000000..c931b47221 --- /dev/null +++ b/test/functional/eval/executable_spec.lua @@ -0,0 +1,156 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq, clear, call, iswin, write_file = + helpers.eq, helpers.clear, helpers.call, helpers.iswin, helpers.write_file + +describe('executable()', function() + before_each(clear) + + it('returns 1 for commands in $PATH', function() + local exe = iswin() and 'ping' or 'ls' + eq(1, call('executable', exe)) + end) + + it('returns 0 for non-existent files', function() + eq(0, call('executable', 'no_such_file_exists_209ufq23f')) + end) + + it('sibling to nvim binary', function() + -- Some executable in build/bin/, *not* in $PATH nor CWD. + local sibling_exe = 'printargs-test' + -- Windows: siblings are in Nvim's "pseudo-$PATH". + local expected = iswin() and 1 or 0 + if iswin() then + -- $PATH on AppVeyor CI might be oversized, redefine it to a minimal one. + clear({env={PATH=[[C:\Windows\system32;C:\Windows]]}}) + eq('arg1=lemon;arg2=sky;arg3=tree;', + call('system', sibling_exe..' lemon sky tree')) + end + local is_executable = call('executable', sibling_exe) + if iswin() and is_executable ~= expected then + pending('XXX: sometimes fails on AppVeyor') + end + end) + + describe('exec-bit', function() + setup(function() + clear() + write_file('Xtest_not_executable', 'non-executable file') + write_file('Xtest_executable', 'executable file (exec-bit set)') + if not iswin() then -- N/A for Windows. + call('system', {'chmod', '-x', 'Xtest_not_executable'}) + call('system', {'chmod', '+x', 'Xtest_executable'}) + end + end) + + teardown(function() + os.remove('Xtest_not_executable') + os.remove('Xtest_executable') + end) + + it('not set', function() + local expected = iswin() and 1 or 0 + eq(expected, call('executable', 'Xtest_not_executable')) + eq(expected, call('executable', './Xtest_not_executable')) + end) + + it('set, unqualified and not in $PATH', function() + local expected = iswin() and 1 or 0 + eq(expected, call('executable', 'Xtest_executable')) + end) + + it('set, qualified as a path', function() + eq(1, call('executable', './Xtest_executable')) + end) + end) +end) + +describe('executable() (Windows)', function() + if not iswin() then return end -- N/A for Unix. + + local exts = {'bat', 'exe', 'com', 'cmd'} + setup(function() + for _, ext in ipairs(exts) do + write_file('test_executable_'..ext..'.'..ext, '') + end + write_file('test_executable_zzz.zzz', '') + end) + + teardown(function() + for _, ext in ipairs(exts) do + os.remove('test_executable_'..ext..'.'..ext) + end + os.remove('test_executable_zzz.zzz') + end) + + it('tries default extensions on a filename if $PATHEXT is empty', function() + -- Empty $PATHEXT defaults to ".com;.exe;.bat;.cmd". + clear({env={PATHEXT=''}}) + for _,ext in ipairs(exts) do + eq(1, call('executable', 'test_executable_'..ext)) + end + eq(0, call('executable', 'test_executable_zzz')) + end) + + it('tries default extensions on a filepath if $PATHEXT is empty', function() + -- Empty $PATHEXT defaults to ".com;.exe;.bat;.cmd". + clear({env={PATHEXT=''}}) + for _,ext in ipairs(exts) do + eq(1, call('executable', '.\\test_executable_'..ext)) + end + eq(0, call('executable', '.\\test_executable_zzz')) + end) + + it('full path with extension', function() + -- Some executable we can expect in the test env. + local exe = 'printargs-test' + local exedir = helpers.eval("fnamemodify(v:progpath, ':h')") + local exepath = exedir..'/'..exe..'.exe' + eq(1, call('executable', exepath)) + eq('arg1=lemon;arg2=sky;arg3=tree;', + call('system', exepath..' lemon sky tree')) + end) + + it('full path without extension', function() + -- Some executable we can expect in the test env. + local exe = 'printargs-test' + local exedir = helpers.eval("fnamemodify(v:progpath, ':h')") + local exepath = exedir..'/'..exe + eq('arg1=lemon;arg2=sky;arg3=tree;', + call('system', exepath..' lemon sky tree')) + eq(1, call('executable', exepath)) + end) + + it('respects $PATHEXT when trying extensions on a filename', function() + clear({env={PATHEXT='.zzz'}}) + for _,ext in ipairs(exts) do + eq(0, call('executable', 'test_executable_'..ext)) + end + eq(1, call('executable', 'test_executable_zzz')) + end) + + it('respects $PATHEXT when trying extensions on a filepath', function() + clear({env={PATHEXT='.zzz'}}) + for _,ext in ipairs(exts) do + eq(0, call('executable', '.\\test_executable_'..ext)) + end + eq(1, call('executable', '.\\test_executable_zzz')) + end) + + it('returns 1 for any existing filename', function() + clear({env={PATHEXT=''}}) + for _,ext in ipairs(exts) do + eq(1, call('executable', 'test_executable_'..ext..'.'..ext)) + end + eq(1, call('executable', 'test_executable_zzz.zzz')) + end) + + it('returns 1 for any existing path (backslashes)', function() + clear({env={PATHEXT=''}}) + for _,ext in ipairs(exts) do + eq(1, call('executable', '.\\test_executable_'..ext..'.'..ext)) + eq(1, call('executable', './test_executable_'..ext..'.'..ext)) + end + eq(1, call('executable', '.\\test_executable_zzz.zzz')) + eq(1, call('executable', './test_executable_zzz.zzz')) + end) +end) diff --git a/test/functional/eval/execute_spec.lua b/test/functional/eval/execute_spec.lua new file mode 100644 index 0000000000..af37ab8d55 --- /dev/null +++ b/test/functional/eval/execute_spec.lua @@ -0,0 +1,206 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local eval = helpers.eval +local clear = helpers.clear +local source = helpers.source +local redir_exec = helpers.redir_exec +local exc_exec = helpers.exc_exec +local funcs = helpers.funcs +local Screen = require('test.functional.ui.screen') +local command = helpers.command +local feed = helpers.feed +local iswin = helpers.iswin + +describe('execute()', function() + before_each(clear) + + it('captures the same result as :redir', function() + eq(redir_exec('messages'), funcs.execute('messages')) + end) + + it('captures the concatenated outputs of a List of commands', function() + eq("foobar", funcs.execute({'echon "foo"', 'echon "bar"'})) + eq("\nfoo\nbar", funcs.execute({'echo "foo"', 'echo "bar"'})) + end) + + it('supports nested execute("execute(...)")', function() + eq('42', funcs.execute([[echon execute("echon execute('echon 42')")]])) + end) + + it('supports nested :redir to a variable', function() + source([[ + function! g:Foo() + let a = '' + redir => a + silent echon "foo" + redir END + return a + endfunction + function! g:Bar() + let a = '' + redir => a + silent echon "bar1" + call g:Foo() + silent echon "bar2" + redir END + silent echon "bar3" + return a + endfunction + ]]) + eq('top1bar1foobar2bar3', funcs.execute('echon "top1"|call g:Bar()')) + end) + + it('supports nested :redir to a register', function() + source([[ + let @a = '' + function! g:Foo() + redir @a>> + silent echon "foo" + redir END + return @a + endfunction + function! g:Bar() + redir @a>> + silent echon "bar1" + call g:Foo() + silent echon "bar2" + redir END + silent echon "bar3" + return @a + endfunction + ]]) + eq('top1bar1foobar2bar3', funcs.execute('echon "top1"|call g:Bar()')) + -- :redir itself doesn't nest, so the redirection ends in g:Foo + eq('bar1foo', eval('@a')) + end) + + it('captures a transformed string', function() + eq('^A', funcs.execute('echon "\\<C-a>"')) + end) + + it('returns empty string if the argument list is empty', function() + eq('', funcs.execute({})) + eq(0, exc_exec('let g:ret = execute(v:_null_list)')) + eq('', eval('g:ret')) + end) + + it('captures errors', function() + local ret + ret = exc_exec('call execute(0.0)') + eq('Vim(call):E806: using Float as a String', ret) + ret = exc_exec('call execute(v:_null_dict)') + eq('Vim(call):E731: using Dictionary as a String', ret) + ret = exc_exec('call execute(function("tr"))') + eq('Vim(call):E729: using Funcref as a String', ret) + ret = exc_exec('call execute(["echo 42", 0.0, "echo 44"])') + eq('Vim:E806: using Float as a String', ret) + ret = exc_exec('call execute(["echo 42", v:_null_dict, "echo 44"])') + eq('Vim:E731: using Dictionary as a String', ret) + ret = exc_exec('call execute(["echo 42", function("tr"), "echo 44"])') + eq('Vim:E729: using Funcref as a String', ret) + end) + + it('captures output with highlights', function() + eq('\nErrorMsg xxx ctermfg=15 ctermbg=1 guifg=White guibg=Red', + eval('execute("hi ErrorMsg")')) + end) + + it('does not corrupt the command display #5422', function() + 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 | + {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) + + -- This deviates from vim behavior, but is consistent + -- with how nvim currently displays the output. + it('does capture shell-command output', function() + local win_lf = iswin() and '\13' or '' + eq('\n:!echo foo\r\n\nfoo'..win_lf..'\n', funcs.execute('!echo foo')) + end) + + describe('{silent} argument', function() + it('captures & displays output for ""', function() + local screen = Screen.new(40, 5) + screen:attach() + command('let g:mes = execute("echon 42", "")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + 42 | + ]]) + eq('42', eval('g:mes')) + end) + + it('captures but does not display output for "silent"', function() + local screen = Screen.new(40, 5) + screen:attach() + command('let g:mes = execute("echon 42")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]]) + eq('42', eval('g:mes')) + + command('let g:mes = execute("echon 13", "silent")') + screen:expect{grid=[[ + ^ | + ~ | + ~ | + ~ | + | + ]], unchanged=true} + eq('13', eval('g:mes')) + end) + + it('suppresses errors for "silent!"', function() + eq(0, exc_exec('let g:mes = execute(0.0, "silent!")')) + eq('', eval('g:mes')) + + eq(0, exc_exec('let g:mes = execute("echon add(1, 1)", "silent!")')) + eq('1', eval('g:mes')) + + eq(0, exc_exec('let g:mes = execute(["echon 42", "echon add(1, 1)"], "silent!")')) + eq('421', eval('g:mes')) + end) + + it('propagates errors for "" and "silent"', function() + local ret + ret = exc_exec('call execute(0.0, "")') + eq('Vim(call):E806: using Float as a String', ret) + + ret = exc_exec('call execute(v:_null_dict, "silent")') + eq('Vim(call):E731: using Dictionary as a String', ret) + + ret = exc_exec('call execute("echo add(1, 1)", "")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute("echo add(1, 1)", "silent")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "silent")') + eq('Vim(echo):E714: List required', ret) + end) + end) +end) diff --git a/test/functional/eval/fnamemodify_spec.lua b/test/functional/eval/fnamemodify_spec.lua new file mode 100644 index 0000000000..fe6b50a544 --- /dev/null +++ b/test/functional/eval/fnamemodify_spec.lua @@ -0,0 +1,39 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eq = helpers.eq +local iswin = helpers.iswin +local fnamemodify = helpers.funcs.fnamemodify +local command = helpers.command +local write_file = helpers.write_file + +describe('fnamemodify()', function() + setup(function() + write_file('Xtest-fnamemodify.txt', [[foobar]]) + end) + + before_each(clear) + + teardown(function() + os.remove('Xtest-fnamemodify.txt') + end) + + it('works', function() + local root = helpers.pathroot() + eq(root, fnamemodify([[/]], ':p:h')) + eq(root, fnamemodify([[/]], ':p')) + if iswin() then + eq(root, fnamemodify([[\]], ':p:h')) + eq(root, fnamemodify([[\]], ':p')) + command('set shellslash') + root = string.sub(root, 1, -2)..'/' + eq(root, fnamemodify([[\]], ':p:h')) + eq(root, fnamemodify([[\]], ':p')) + eq(root, fnamemodify([[/]], ':p:h')) + eq(root, fnamemodify([[/]], ':p')) + end + end) + + it(':8 works', function() + eq('Xtest-fnamemodify.txt', fnamemodify([[Xtest-fnamemodify.txt]], ':8')) + end) +end) diff --git a/test/functional/eval/function_spec.lua b/test/functional/eval/function_spec.lua new file mode 100644 index 0000000000..776e760aaf --- /dev/null +++ b/test/functional/eval/function_spec.lua @@ -0,0 +1,29 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local eq = helpers.eq +local exc_exec = helpers.exc_exec + +describe('Up to MAX_FUNC_ARGS arguments are handled by', function() + local max_func_args = 20 -- from eval.h + local range = helpers.funcs.range + + before_each(clear) + + it('printf()', function() + local printf = helpers.funcs.printf + local rep = helpers.funcs['repeat'] + local expected = '2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,' + eq(expected, printf(rep('%d,', max_func_args-1), unpack(range(2, max_func_args)))) + local ret = exc_exec('call printf("", 2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)') + eq('Vim(call):E740: Too many arguments for function printf', ret) + end) + + it('rpcnotify()', function() + local rpcnotify = helpers.funcs.rpcnotify + local ret = rpcnotify(0, 'foo', unpack(range(3, max_func_args))) + eq(1, ret) + ret = exc_exec('call rpcnotify(0, "foo", 3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)') + eq('Vim(call):E740: Too many arguments for function rpcnotify', ret) + end) +end) diff --git a/test/functional/eval/getline_spec.lua b/test/functional/eval/getline_spec.lua new file mode 100644 index 0000000000..3c56bde094 --- /dev/null +++ b/test/functional/eval/getline_spec.lua @@ -0,0 +1,39 @@ +local helpers = require('test.functional.helpers')(after_each) + +local call = helpers.call +local clear = helpers.clear +local eq = helpers.eq +local expect = helpers.expect + +describe('getline()', function() + before_each(function() + clear() + call('setline', 1, {'a', 'b', 'c'}) + expect([[ + a + b + c]]) + end) + + it('returns empty string for invalid line', function() + eq('', call('getline', -1)) + eq('', call('getline', 0)) + eq('', call('getline', 4)) + end) + + it('returns empty list for invalid range', function() + eq({}, call('getline', 2, 1)) + eq({}, call('getline', -1, 1)) + eq({}, call('getline', 4, 4)) + end) + + it('returns value of valid line', function() + eq('b', call('getline', 2)) + eq('a', call('getline', '.')) + end) + + it('returns value of valid range', function() + eq({'a', 'b'}, call('getline', 1, 2)) + eq({'a', 'b', 'c'}, call('getline', 1, 4)) + end) +end) diff --git a/test/functional/eval/glob_spec.lua b/test/functional/eval/glob_spec.lua index 599b3dcdc3..b8807ecfcc 100644 --- a/test/functional/eval/glob_spec.lua +++ b/test/functional/eval/glob_spec.lua @@ -1,13 +1,13 @@ local lfs = require('lfs') local helpers = require('test.functional.helpers')(after_each) -local clear, execute, eval, eq = helpers.clear, helpers.execute, helpers.eval, helpers.eq +local clear, command, eval, eq = helpers.clear, helpers.command, helpers.eval, helpers.eq before_each(function() clear() lfs.mkdir('test-glob') -- Long path might cause "Press ENTER" prompt; use :silent to avoid it. - execute('silent cd test-glob') + command('silent cd test-glob') end) after_each(function() diff --git a/test/functional/eval/has_spec.lua b/test/functional/eval/has_spec.lua new file mode 100644 index 0000000000..a3af2d1a20 --- /dev/null +++ b/test/functional/eval/has_spec.lua @@ -0,0 +1,66 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local clear = helpers.clear +local funcs = helpers.funcs +local iswin = helpers.iswin + +describe('has()', function() + before_each(clear) + + it('"nvim-x.y.z"', function() + eq(0, funcs.has("nvim-")) + eq(0, funcs.has("nvim- ")) + eq(0, funcs.has("nvim- \t ")) + eq(0, funcs.has("nvim-0. 1. 1")) + eq(0, funcs.has("nvim-0. 1.1")) + eq(0, funcs.has("nvim-0.1. 1")) + eq(0, funcs.has("nvim-a")) + eq(0, funcs.has("nvim-a.b.c")) + eq(0, funcs.has("nvim-0.b.c")) + eq(0, funcs.has("nvim-0.0.c")) + eq(0, funcs.has("nvim-0.b.0")) + eq(0, funcs.has("nvim-a.b.0")) + eq(0, funcs.has("nvim-.0.0.0")) + eq(0, funcs.has("nvim-.0")) + eq(0, funcs.has("nvim-0.")) + eq(0, funcs.has("nvim-0..")) + eq(0, funcs.has("nvim-.")) + eq(0, funcs.has("nvim-..")) + eq(0, funcs.has("nvim-...")) + eq(0, funcs.has("nvim-42")) + eq(0, funcs.has("nvim-9999")) + eq(0, funcs.has("nvim-99.001.05")) + + eq(1, funcs.has("nvim")) + eq(1, funcs.has("nvim-0")) + eq(1, funcs.has("nvim-0.1")) + eq(1, funcs.has("nvim-0.0.0")) + eq(1, funcs.has("nvim-0.1.1.")) + eq(1, funcs.has("nvim-0.1.1.abc")) + eq(1, funcs.has("nvim-0.1.1..")) + eq(1, funcs.has("nvim-0.1.1.. ..")) + eq(1, funcs.has("nvim-0.1.1.... ")) + eq(1, funcs.has("nvim-0.0.0")) + eq(1, funcs.has("nvim-0.0.1")) + eq(1, funcs.has("nvim-0.1.0")) + eq(1, funcs.has("nvim-0.1.1")) + eq(1, funcs.has("nvim-0.1.5")) + eq(1, funcs.has("nvim-0000.001.05")) + eq(1, funcs.has("nvim-0.01.005")) + eq(1, funcs.has("nvim-00.001.05")) + end) + + it('"unnamedplus"', function() + if (not iswin()) and funcs.has("clipboard") == 1 then + eq(1, funcs.has("unnamedplus")) + else + eq(0, funcs.has("unnamedplus")) + end + end) + + it('"wsl"', function() + if 1 == funcs.has('win32') or 1 == funcs.has('mac') then + eq(0, funcs.has('wsl')) + end + end) +end) diff --git a/test/functional/eval/hostname_spec.lua b/test/functional/eval/hostname_spec.lua new file mode 100644 index 0000000000..6112cf64e3 --- /dev/null +++ b/test/functional/eval/hostname_spec.lua @@ -0,0 +1,20 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local ok = helpers.ok +local call = helpers.call +local clear = helpers.clear +local iswin = helpers.iswin + +describe('hostname()', function() + before_each(clear) + + it('returns hostname string', function() + local actual = call('hostname') + ok(string.len(actual) > 0) + if call('executable', 'hostname') == 1 then + local expected = string.gsub(call('system', 'hostname'), '[\n\r]', '') + eq((iswin() and expected:upper() or expected), + (iswin() and actual:upper() or actual)) + end + end) +end) diff --git a/test/functional/eval/input_spec.lua b/test/functional/eval/input_spec.lua new file mode 100644 index 0000000000..e774b939f7 --- /dev/null +++ b/test/functional/eval/input_spec.lua @@ -0,0 +1,483 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local eq = helpers.eq +local feed = helpers.feed +local meths = helpers.meths +local clear = helpers.clear +local source = helpers.source +local command = helpers.command +local exc_exec = helpers.exc_exec +local nvim_async = helpers.nvim_async + +local screen + +before_each(function() + clear() + screen = Screen.new(25, 5) + screen:attach() + source([[ + hi Test ctermfg=Red guifg=Red term=bold + function CustomCompl(...) + return 'TEST' + endfunction + function CustomListCompl(...) + return ['FOO'] + endfunction + + highlight RBP1 guibg=Red + highlight RBP2 guibg=Yellow + highlight RBP3 guibg=Green + highlight RBP4 guibg=Blue + let g:NUM_LVLS = 4 + function Redraw() + redraw! + return '' + endfunction + cnoremap <expr> {REDRAW} Redraw() + function RainBowParens(cmdline) + let ret = [] + let i = 0 + let lvl = 0 + while i < len(a:cmdline) + if a:cmdline[i] is# '(' + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + let lvl += 1 + elseif a:cmdline[i] is# ')' + let lvl -= 1 + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + endif + let i += 1 + endwhile + return ret + endfunction + ]]) + screen:set_default_attr_ids({ + EOB={bold = true, foreground = Screen.colors.Blue1}, + T={foreground=Screen.colors.Red}, + RBP1={background=Screen.colors.Red}, + RBP2={background=Screen.colors.Yellow}, + RBP3={background=Screen.colors.Green}, + RBP4={background=Screen.colors.Blue}, + SEP={bold = true, reverse = true}, + CONFIRM={bold = true, foreground = Screen.colors.SeaGreen4}, + }) +end) + +describe('input()', function() + it('works with multiline prompts', function() + feed([[:call input("Test\nFoo")<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + Test | + Foo^ | + ]]) + end) + it('works with multiline prompts and :echohl', function() + feed([[:echohl Test | call input("Test\nFoo")<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + {T:Test} | + {T:Foo}^ | + ]]) + command('redraw!') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo}^ | + ]]) + end) + it('allows unequal numeric arguments when using multiple args', function() + command('echohl Test') + feed([[:call input(1, 2)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}2^ | + ]]) + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}^ | + ]]) + end) + it('allows unequal numeric values when using {opts} dictionary', function() + command('echohl Test') + meths.set_var('opts', {prompt=1, default=2, cancelreturn=3}) + feed([[:echo input(opts)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}2^ | + ]]) + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}^ | + ]]) + feed('<Esc>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:3} | + ]]) + end) + it('works with redraw', function() + command('echohl Test') + meths.set_var('opts', {prompt='Foo>', default='Bar'}) + feed([[:echo inputdialog(opts)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Bar^ | + ]]) + command('mode') + screen:expect{grid=[[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Bar^ | + ]], reset=true} + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Ba^ | + ]]) + command('mode') + screen:expect{grid=[[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Ba^ | + ]], reset=true} + end) + it('allows omitting everything with dictionary argument', function() + command('echohl Test') + feed([[:call input({})<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + ^ | + ]]) + end) + it('supports completion', function() + feed(':let var = input("", "", "custom,CustomCompl")<CR>') + feed('<Tab><CR>') + eq('TEST', meths.get_var('var')) + + feed(':let var = input({"completion": "customlist,CustomListCompl"})<CR>') + feed('<Tab><CR>') + eq('FOO', meths.get_var('var')) + end) + it('supports cancelreturn', function() + feed(':let var = input({"cancelreturn": "BAR"})<CR>') + feed('<Esc>') + eq('BAR', meths.get_var('var')) + end) + it('supports default string', function() + feed(':let var = input("", "DEF1")<CR>') + feed('<CR>') + eq('DEF1', meths.get_var('var')) + + feed(':let var = input({"default": "DEF2"})<CR>') + feed('<CR>') + eq('DEF2', meths.get_var('var')) + end) + it('errors out on invalid inputs', function() + eq('Vim(call):E730: using List as a String', + exc_exec('call input([])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input("", [])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input("", "", [])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input({"prompt": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input({"cancelreturn": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input({"default": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call input({"completion": []})')) + eq('Vim(call):E5050: {opts} must be the only argument', + exc_exec('call input({}, "default")')) + eq('Vim(call):E118: Too many arguments for function: input', + exc_exec('call input("prompt> ", "default", "file", "extra")')) + end) + it('supports highlighting', function() + command('nnoremap <expr> X input({"highlight": "RainBowParens"})[-1]') + feed([[X]]) + feed('(())') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {RBP1:(}{RBP2:()}{RBP1:)}^ | + ]]) + end) + it('is not hidden by :silent', function() + feed([[:silent call input('Foo: ')<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + Foo: ^ | + | + ]]) + feed('Bar') + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + Foo: Bar^ | + | + ]]) + feed('<CR>') + end) +end) +describe('inputdialog()', function() + it('works with multiline prompts', function() + feed([[:call inputdialog("Test\nFoo")<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + Test | + Foo^ | + ]]) + end) + it('works with multiline prompts and :echohl', function() + feed([[:echohl Test | call inputdialog("Test\nFoo")<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {SEP: }| + {T:Test} | + {T:Foo}^ | + ]]) + command('redraw!') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo}^ | + ]]) + end) + it('allows unequal numeric arguments when using multiple args', function() + command('echohl Test') + feed([[:call inputdialog(1, 2)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}2^ | + ]]) + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}^ | + ]]) + end) + it('allows unequal numeric values when using {opts} dictionary', function() + command('echohl Test') + meths.set_var('opts', {prompt=1, default=2, cancelreturn=3}) + feed([[:echo input(opts)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}2^ | + ]]) + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:1}^ | + ]]) + feed('<Esc>') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:3} | + ]]) + end) + it('works with redraw', function() + command('echohl Test') + meths.set_var('opts', {prompt='Foo>', default='Bar'}) + feed([[:echo input(opts)<CR>]]) + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Bar^ | + ]]) + command('mode') + screen:expect{grid=[[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Bar^ | + ]], reset=true} + feed('<BS>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Ba^ | + ]]) + command('mode') + screen:expect{grid=[[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {T:Foo>}Ba^ | + ]], reset=true} + end) + it('allows omitting everything with dictionary argument', function() + command('echohl Test') + feed(':echo inputdialog({})<CR>') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + ^ | + ]]) + end) + it('supports completion', function() + feed(':let var = inputdialog({"completion": "customlist,CustomListCompl"})<CR>') + feed('<Tab><CR>') + eq('FOO', meths.get_var('var')) + end) + it('supports cancelreturn', function() + feed(':let var = inputdialog("", "", "CR1")<CR>') + feed('<Esc>') + eq('CR1', meths.get_var('var')) + + feed(':let var = inputdialog({"cancelreturn": "BAR"})<CR>') + feed('<Esc>') + eq('BAR', meths.get_var('var')) + end) + it('supports default string', function() + feed(':let var = inputdialog("", "DEF1")<CR>') + feed('<CR>') + eq('DEF1', meths.get_var('var')) + + feed(':let var = inputdialog({"default": "DEF2"})<CR>') + feed('<CR>') + eq('DEF2', meths.get_var('var')) + end) + it('errors out on invalid inputs', function() + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog([])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog("", [])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog("", "", [])')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog({"prompt": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog({"cancelreturn": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog({"default": []})')) + eq('Vim(call):E730: using List as a String', + exc_exec('call inputdialog({"completion": []})')) + eq('Vim(call):E5050: {opts} must be the only argument', + exc_exec('call inputdialog({}, "default")')) + eq('Vim(call):E118: Too many arguments for function: inputdialog', + exc_exec('call inputdialog("prompt> ", "default", "file", "extra")')) + end) + it('supports highlighting', function() + command('nnoremap <expr> X inputdialog({"highlight": "RainBowParens"})[-1]') + feed([[X]]) + feed('(())') + screen:expect([[ + | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + {RBP1:(}{RBP2:()}{RBP1:)}^ | + ]]) + end) +end) + +describe('confirm()', function() + it("shows dialog even if :silent #8788", function() + command("autocmd BufNewFile * call confirm('test')") + + local function check_and_clear(edit_line) + screen:expect([[ + | + {SEP: }| + ]]..edit_line..[[ + {CONFIRM:test} | + {CONFIRM:[O]k: }^ | + ]]) + feed('<cr>') + command('redraw') + command('bdelete!') + end + + -- With shortmess-=F + command('set shortmess-=F') + feed(':edit foo<cr>') + check_and_clear('"foo" [New File] |\n') + + -- With shortmess+=F + command('set shortmess+=F') + feed(':edit foo<cr>') + check_and_clear(':edit foo |\n') + + -- With :silent + feed(':silent edit foo<cr>') + check_and_clear(':silent edit foo |\n') + + -- With API (via eval/VimL) call and shortmess+=F + feed(':call nvim_command("edit x")<cr>') + check_and_clear(':call nvim_command("edit |\n') + + nvim_async('command', 'edit x') + check_and_clear(' |\n') + end) +end) diff --git a/test/functional/eval/interrupt_spec.lua b/test/functional/eval/interrupt_spec.lua new file mode 100644 index 0000000000..7f4ca95317 --- /dev/null +++ b/test/functional/eval/interrupt_spec.lua @@ -0,0 +1,61 @@ +local helpers = require('test.functional.helpers')(after_each) + +local command = helpers.command +local meths = helpers.meths +local clear = helpers.clear +local sleep = helpers.sleep +local wait = helpers.wait +local feed = helpers.feed +local eq = helpers.eq + +local dur +local min_dur = 8 +local len = 131072 + +describe('List support code', function() + if not pending('does not actually allows interrupting with just got_int', function() end) then return end + -- The following tests are confirmed to work with os_breakcheck() just before + -- `if (got_int) {break;}` in tv_list_copy and list_join_inner() and not to + -- work without. + setup(function() + clear() + dur = 0 + while true do + command(([[ + let rt = reltime() + let bl = range(%u) + let dur = reltimestr(reltime(rt)) + ]]):format(len)) + dur = tonumber(meths.get_var('dur')) + if dur >= min_dur then + -- print(('Using len %u, dur %g'):format(len, dur)) + break + else + len = len * 2 + end + end + end) + it('allows interrupting copy', function() + feed(':let t_rt = reltime()<CR>:let t_bl = copy(bl)<CR>') + sleep(min_dur / 16 * 1000) + feed('<C-c>') + wait() + command('let t_dur = reltimestr(reltime(t_rt))') + local t_dur = tonumber(meths.get_var('t_dur')) + if t_dur >= dur / 8 then + eq(nil, ('Took too long to cancel: %g >= %g'):format(t_dur, dur / 8)) + end + end) + it('allows interrupting join', function() + feed(':let t_rt = reltime()<CR>:let t_j = join(bl)<CR>') + sleep(min_dur / 16 * 1000) + feed('<C-c>') + wait() + command('let t_dur = reltimestr(reltime(t_rt))') + local t_dur = tonumber(meths.get_var('t_dur')) + print(('t_dur: %g'):format(t_dur)) + if t_dur >= dur / 8 then + eq(nil, ('Took too long to cancel: %g >= %g'):format(t_dur, dur / 8)) + end + end) +end) diff --git a/test/functional/eval/json_functions_spec.lua b/test/functional/eval/json_functions_spec.lua index 159d775ff1..8dcaea806e 100644 --- a/test/functional/eval/json_functions_spec.lua +++ b/test/functional/eval/json_functions_spec.lua @@ -4,16 +4,17 @@ local funcs = helpers.funcs local meths = helpers.meths local eq = helpers.eq local eval = helpers.eval -local execute = helpers.execute +local command = helpers.command local exc_exec = helpers.exc_exec local redir_exec = helpers.redir_exec local NIL = helpers.NIL +local source = helpers.source describe('json_decode() function', function() local restart = function(...) clear(...) - execute('language C') - execute([[ + source([[ + language C function Eq(exp, act) let act = a:act let exp = a:exp @@ -45,8 +46,6 @@ describe('json_decode() function', function() endif return 1 endfunction - ]]) - execute([[ function EvalEq(exp, act_expr) let act = eval(a:act_expr) if Eq(a:exp, act) @@ -441,7 +440,7 @@ describe('json_decode() function', function() local sp_decode_eq = function(expected, json) meths.set_var('__json', json) speq(expected, 'json_decode(g:__json)') - execute('unlet! g:__json') + command('unlet! g:__json') end it('parses strings with NUL properly', function() @@ -489,18 +488,6 @@ describe('json_decode() function', function() '{"b": 3, "a": 1, "c": 4, "d": 2, "\\u0000": 4}') end) - it('converts strings to latin1 when &encoding is latin1', function() - restart('--cmd', 'set encoding=latin1') - eq('\171', funcs.json_decode('"\\u00AB"')) - sp_decode_eq({_TYPE='string', _VAL={'\n\171\n'}}, '"\\u0000\\u00AB\\u0000"') - end) - - it('fails to convert string to latin1 if it is impossible', function() - restart('--cmd', 'set encoding=latin1') - eq('Vim(call):E474: Failed to convert string "ꯍ" from UTF-8', - exc_exec('call json_decode(\'"\\uABCD"\')')) - end) - it('parses U+00C3 correctly', function() eq('\195\131', funcs.json_decode('"\195\131"')) end) @@ -528,14 +515,6 @@ describe('json_decode() function', function() eq({key={'val', 'val2'}, key2=1}, funcs.json_decode(str)) end) - it('always treats input as UTF-8', function() - -- When &encoding is latin1 string "«" is U+00C2 U+00AB U+00C2: «Â. So if - -- '"«"' was parsed as latin1 json_decode would return three characters, and - -- only one U+00AB when this string is parsed as latin1. - restart('--cmd', 'set encoding=latin1') - eq(('%c'):format(0xAB), funcs.json_decode('"«"')) - end) - it('does not overflow when writing error message about decoding ["", ""]', function() eq('\nE474: Attempt to decode a blank string' @@ -547,7 +526,7 @@ end) describe('json_encode() function', function() before_each(function() clear() - execute('language C') + command('language C') end) it('dumps strings', function() @@ -596,94 +575,94 @@ describe('json_encode() function', function() it('cannot dump generic mapping with generic mapping keys and values', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('call add(todump._VAL, [todumpv1, todumpv2])') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('call add(todump._VAL, [todumpv1, todumpv2])') eq('Vim(call):E474: Invalid key in special dictionary', exc_exec('call json_encode(todump)')) end) it('cannot dump generic mapping with ext key', function() - execute('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('Vim(call):E474: Invalid key in special dictionary', exc_exec('call json_encode(todump)')) end) it('cannot dump generic mapping with array key', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('Vim(call):E474: Invalid key in special dictionary', exc_exec('call json_encode(todump)')) end) it('cannot dump generic mapping with UINT64_MAX key', function() - execute('let todump = {"_TYPE": v:msgpack_types.integer}') - execute('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.integer}') + command('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('Vim(call):E474: Invalid key in special dictionary', exc_exec('call json_encode(todump)')) end) it('cannot dump generic mapping with floating-point key', function() - execute('let todump = {"_TYPE": v:msgpack_types.float, "_VAL": 0.125}') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.float, "_VAL": 0.125}') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('Vim(call):E474: Invalid key in special dictionary', exc_exec('call json_encode(todump)')) end) it('can dump generic mapping with STR special key and NUL', function() - execute('let todump = {"_TYPE": v:msgpack_types.string, "_VAL": ["\\n"]}') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.string, "_VAL": ["\\n"]}') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('{"\\u0000": 1}', eval('json_encode(todump)')) end) it('can dump generic mapping with BIN special key and NUL', function() - execute('let todump = {"_TYPE": v:msgpack_types.binary, "_VAL": ["\\n"]}') - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') + command('let todump = {"_TYPE": v:msgpack_types.binary, "_VAL": ["\\n"]}') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[todump, 1]]}') eq('{"\\u0000": 1}', eval('json_encode(todump)')) end) it('can dump STR special mapping with NUL and NL', function() - execute('let todump = {"_TYPE": v:msgpack_types.string, "_VAL": ["\\n", ""]}') + command('let todump = {"_TYPE": v:msgpack_types.string, "_VAL": ["\\n", ""]}') eq('"\\u0000\\n"', eval('json_encode(todump)')) end) it('can dump BIN special mapping with NUL and NL', function() - execute('let todump = {"_TYPE": v:msgpack_types.binary, "_VAL": ["\\n", ""]}') + command('let todump = {"_TYPE": v:msgpack_types.binary, "_VAL": ["\\n", ""]}') eq('"\\u0000\\n"', eval('json_encode(todump)')) end) it('cannot dump special ext mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') + command('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') eq('Vim(call):E474: Unable to convert EXT string to JSON', exc_exec('call json_encode(todump)')) end) it('can dump special array mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') eq('[5, [""]]', eval('json_encode(todump)')) end) it('can dump special UINT64_MAX mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.integer}') - execute('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') + command('let todump = {"_TYPE": v:msgpack_types.integer}') + command('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') eq('18446744073709551615', eval('json_encode(todump)')) end) it('can dump special INT64_MIN mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.integer}') - execute('let todump._VAL = [-1, 2, 0, 0]') + command('let todump = {"_TYPE": v:msgpack_types.integer}') + command('let todump._VAL = [-1, 2, 0, 0]') eq('-9223372036854775808', eval('json_encode(todump)')) end) it('can dump special BOOLEAN true mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 1}') + command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 1}') eq('true', eval('json_encode(todump)')) end) it('can dump special BOOLEAN false mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 0}') + command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 0}') eq('false', eval('json_encode(todump)')) end) it('can dump special NIL mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.nil, "_VAL": 0}') + command('let todump = {"_TYPE": v:msgpack_types.nil, "_VAL": 0}') eq('null', eval('json_encode(todump)')) end) @@ -692,62 +671,68 @@ describe('json_encode() function', function() exc_exec('call json_encode(function("tr"))')) end) + it('fails to dump a partial', function() + command('function T() dict\nendfunction') + eq('Vim(call):E474: Error while dumping encode_tv2json() argument, itself: attempt to dump function reference', + exc_exec('call json_encode(function("T", [1, 2], {}))')) + end) + it('fails to dump a function reference in a list', function() eq('Vim(call):E474: Error while dumping encode_tv2json() argument, index 0: attempt to dump function reference', exc_exec('call json_encode([function("tr")])')) end) it('fails to dump a recursive list', function() - execute('let todump = [[[]]]') - execute('call add(todump[0][0], todump)') + command('let todump = [[[]]]') + command('call add(todump[0][0], todump)') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode(todump)')) end) it('fails to dump a recursive dict', function() - execute('let todump = {"d": {"d": {}}}') - execute('call extend(todump.d.d, {"d": todump})') + command('let todump = {"d": {"d": {}}}') + command('call extend(todump.d.d, {"d": todump})') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode([todump])')) end) it('can dump dict with two same dicts inside', function() - execute('let inter = {}') - execute('let todump = {"a": inter, "b": inter}') + command('let inter = {}') + command('let todump = {"a": inter, "b": inter}') eq('{"a": {}, "b": {}}', eval('json_encode(todump)')) end) it('can dump list with two same lists inside', function() - execute('let inter = []') - execute('let todump = [inter, inter]') + command('let inter = []') + command('let todump = [inter, inter]') eq('[[], []]', eval('json_encode(todump)')) end) it('fails to dump a recursive list in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') - execute('call add(todump._VAL, todump)') + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + command('call add(todump._VAL, todump)') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode(todump)')) end) it('fails to dump a recursive (val) map in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('call add(todump._VAL, ["", todump])') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('call add(todump._VAL, ["", todump])') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode([todump])')) end) it('fails to dump a recursive (val) map in a special dict, _VAL reference', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [["", []]]}') - execute('call add(todump._VAL[0][1], todump._VAL)') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [["", []]]}') + command('call add(todump._VAL[0][1], todump._VAL)') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode(todump)')) end) it('fails to dump a recursive (val) special list in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') - execute('call add(todump._VAL, ["", todump._VAL])') + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + command('call add(todump._VAL, ["", todump._VAL])') eq('Vim(call):E724: unable to correctly dump variable with self-referencing container', exc_exec('call json_encode(todump)')) end) @@ -762,12 +747,6 @@ describe('json_encode() function', function() exc_exec('call json_encode(["", ""], 1)')) end) - it('converts strings from latin1 when &encoding is latin1', function() - clear('--cmd', 'set encoding=latin1') - eq('"\\u00AB"', funcs.json_encode('\171')) - eq('"\\u0000\\u00AB\\u0000"', eval('json_encode({"_TYPE": v:msgpack_types.string, "_VAL": ["\\n\171\\n"]})')) - end) - it('ignores improper values in &isprint', function() meths.set_option('isprint', '1') eq(1, eval('"\1" =~# "\\\\p"')) @@ -797,4 +776,11 @@ describe('json_encode() function', function() it('can dump NULL dictionary', function() eq('{}', eval('json_encode(v:_null_dict)')) end) + + it('fails to parse NULL strings and lists', function() + eq('Vim(call):E474: Attempt to decode a blank string', + exc_exec('call json_decode($XXX_UNEXISTENT_VAR_XXX)')) + eq('Vim(call):E474: Attempt to decode a blank string', + exc_exec('call json_decode(v:_null_list)')) + end) end) diff --git a/test/functional/eval/let_spec.lua b/test/functional/eval/let_spec.lua new file mode 100644 index 0000000000..1bd3405698 --- /dev/null +++ b/test/functional/eval/let_spec.lua @@ -0,0 +1,45 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local clear = helpers.clear +local meths = helpers.meths +local redir_exec = helpers.redir_exec +local source = helpers.source + +before_each(clear) + +describe(':let command', function() + it('correctly lists variables with curly-braces', function() + meths.set_var('v', {0}) + eq('\nv [0]', redir_exec('let {"v"}')) + end) + + it('correctly lists variables with subscript', function() + meths.set_var('v', {0}) + eq('\nv[0] #0', redir_exec('let v[0]')) + eq('\ng:["v"][0] #0', redir_exec('let g:["v"][0]')) + eq('\n{"g:"}["v"][0] #0', redir_exec('let {"g:"}["v"][0]')) + end) + + it(":unlet self-referencing node in a List graph #6070", function() + -- :unlet-ing a self-referencing List must not allow GC on indirectly + -- referenced in-scope Lists. Before #6070 this caused use-after-free. + source([=[ + let [l1, l2] = [[], []] + echo 'l1:' . id(l1) + echo 'l2:' . id(l2) + echo '' + let [l3, l4] = [[], []] + call add(l4, l4) + call add(l4, l3) + call add(l3, 1) + call add(l2, l2) + call add(l2, l1) + call add(l1, 1) + unlet l2 + unlet l4 + call garbagecollect(1) + call feedkeys(":\e:echo l1 l3\n:echo 42\n:cq\n", "t") + ]=]) + end) +end) diff --git a/test/functional/eval/map_functions_spec.lua b/test/functional/eval/map_functions_spec.lua new file mode 100644 index 0000000000..e914f674aa --- /dev/null +++ b/test/functional/eval/map_functions_spec.lua @@ -0,0 +1,159 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local eq = helpers.eq +local eval = helpers.eval +local funcs = helpers.funcs +local nvim = helpers.nvim +local source = helpers.source +local command = helpers.command + +describe('maparg()', function() + before_each(clear) + + local foo_bar_map_table = { + lhs='foo', + silent=0, + rhs='bar', + expr=0, + sid=0, + buffer=0, + nowait=0, + mode='n', + noremap=1, + } + + it('returns a dictionary', function() + nvim('command', 'nnoremap foo bar') + eq('bar', funcs.maparg('foo')) + eq(foo_bar_map_table, funcs.maparg('foo', 'n', false, true)) + end) + + it('returns 1 for silent when <silent> is used', function() + nvim('command', 'nnoremap <silent> foo bar') + eq(1, funcs.maparg('foo', 'n', false, true)['silent']) + + nvim('command', 'nnoremap baz bat') + eq(0, funcs.maparg('baz', 'n', false, true)['silent']) + end) + + it('returns an empty string when no map is present', function() + eq('', funcs.maparg('not a mapping')) + end) + + it('returns an empty dictionary when no map is present and dict is requested', function() + eq({}, funcs.maparg('not a mapping', 'n', false, true)) + end) + + it('returns the same value for noremap and <script>', function() + nvim('command', 'inoremap <script> hello world') + nvim('command', 'inoremap this that') + eq( + funcs.maparg('hello', 'i', false, true)['noremap'], + funcs.maparg('this', 'i', false, true)['noremap'] + ) + end) + + it('returns a boolean for buffer', function() + -- Open enough windows to know we aren't on buffer number 1 + nvim('command', 'new') + nvim('command', 'new') + nvim('command', 'new') + nvim('command', 'cnoremap <buffer> this that') + eq(1, funcs.maparg('this', 'c', false, true)['buffer']) + + -- Global will return 0 always + nvim('command', 'nnoremap other another') + eq(0, funcs.maparg('other', 'n', false, true)['buffer']) + end) + + it('returns script numbers', function() + source([[ + function! s:maparg_test_function() abort + return 'testing' + endfunction + + nnoremap fizz :call <SID>maparg_test_function()<CR> + ]]) + eq(1, funcs.maparg('fizz', 'n', false, true)['sid']) + eq('testing', nvim('call_function', '<SNR>1_maparg_test_function', {})) + end) + + it('works with <F12> and others', function() + source([[ + let g:maparg_test_var = 0 + + nnoremap <F12> :let g:maparg_test_var = 1<CR> + ]]) + eq(0, eval('g:maparg_test_var')) + source([[ + call feedkeys("\<F12>") + ]]) + eq(1, eval('g:maparg_test_var')) + + eq(':let g:maparg_test_var = 1<CR>', funcs.maparg('<F12>', 'n', false, true)['rhs']) + end) + + it('works with <expr>', function() + source([[ + let counter = 0 + inoremap <expr> <C-L> ListItem() + inoremap <expr> <C-R> ListReset() + + func ListItem() + let g:counter += 1 + return g:counter . '. ' + endfunc + + func ListReset() + let g:counter = 0 + return '' + endfunc + + call feedkeys("i\<C-L>") + ]]) + eq(1, eval('g:counter')) + + local map_dict = funcs.maparg('<C-L>', 'i', false, true) + eq(1, map_dict['expr']) + eq('i', map_dict['mode']) + end) + + it('works with combining characters', function() + -- Using addacutes to make combining character better visible + local function ac(s) + local acute = '\204\129' -- U+0301 COMBINING ACUTE ACCENT + local ret = s:gsub('`', acute) + return ret + end + command(ac([[ + nnoremap a b` + nnoremap c` d + nnoremap e` f` + ]])) + eq(ac('b`'), funcs.maparg(ac('a'))) + eq(ac(''), funcs.maparg(ac('c'))) + eq(ac('d'), funcs.maparg(ac('c`'))) + eq(ac('f`'), funcs.maparg(ac('e`'))) + + local function acmap(lhs, rhs) + return { + lhs = ac(lhs), + rhs = ac(rhs), + + buffer = 0, + expr = 0, + mode = 'n', + noremap = 1, + nowait = 0, + sid = 0, + silent = 0, + } + end + + eq({}, funcs.maparg(ac('c'), 'n', 0, 1)) + eq(acmap('a', 'b`'), funcs.maparg(ac('a'), 'n', 0, 1)) + eq(acmap('c`', 'd'), funcs.maparg(ac('c`'), 'n', 0, 1)) + eq(acmap('e`', 'f`'), funcs.maparg(ac('e`'), 'n', 0, 1)) + end) +end) diff --git a/test/functional/eval/match_functions_spec.lua b/test/functional/eval/match_functions_spec.lua new file mode 100644 index 0000000000..0ec465a34c --- /dev/null +++ b/test/functional/eval/match_functions_spec.lua @@ -0,0 +1,158 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local eq = helpers.eq +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) + +describe('setmatches()', function() + it('correctly handles case when both group and pattern entries are numbers', + function() + command('hi def link 1 PreProc') + eq(0, funcs.setmatches({{group=1, pattern=2, id=3, priority=4}})) + eq({{ + group='1', + pattern='2', + id=3, + priority=4, + }}, funcs.getmatches()) + eq(0, funcs.setmatches({{group=1, pattern=2, id=3, priority=4, conceal=5}})) + eq({{ + group='1', + pattern='2', + id=3, + priority=4, + conceal='5', + }}, funcs.getmatches()) + eq(0, funcs.setmatches({{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}})) + eq({{ + group='1', + pos1={2}, + pos2={6}, + id=3, + priority=4, + conceal='5', + }}, funcs.getmatches()) + end) + + it('fails with -1 if highlight group is not defined', function() + expect_err('E28: No such highlight group name: 1', funcs.setmatches, + {{group=1, pattern=2, id=3, priority=4}}) + eq({}, funcs.getmatches()) + 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) + +describe('matchadd()', function() + it('correctly works when first two arguments and conceal are numbers at once', + function() + command('hi def link 1 PreProc') + eq(4, funcs.matchadd(1, 2, 3, 4, {conceal=5})) + eq({{ + group='1', + pattern='2', + priority=3, + id=4, + conceal='5', + }}, funcs.getmatches()) + end) +end) + +describe('matchaddpos()', function() + it('errors out on invalid input', function() + command('hi clear PreProc') + eq('Vim(let):E5030: Empty list at position 0', + exc_exec('let val = matchaddpos("PreProc", [[]])')) + eq('Vim(let):E5030: Empty list at position 1', + exc_exec('let val = matchaddpos("PreProc", [1, v:_null_list])')) + eq('Vim(let):E5031: List or number required at position 1', + exc_exec('let val = matchaddpos("PreProc", [1, v:_null_dict])')) + end) + it('works with 0 lnum', function() + command('hi clear PreProc') + eq(4, funcs.matchaddpos('PreProc', {1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + funcs.matchdelete(4) + eq(4, funcs.matchaddpos('PreProc', {{0}, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + funcs.matchdelete(4) + eq(4, funcs.matchaddpos('PreProc', {0, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + end) + it('works with negative numbers', function() + command('hi clear PreProc') + eq(4, funcs.matchaddpos('PreProc', {-10, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + funcs.matchdelete(4) + eq(4, funcs.matchaddpos('PreProc', {{-10}, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + funcs.matchdelete(4) + eq(4, funcs.matchaddpos('PreProc', {{2, -1}, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + funcs.matchdelete(4) + eq(4, funcs.matchaddpos('PreProc', {{2, 0, -1}, 1}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1}, + priority=3, + id=4, + }}, funcs.getmatches()) + end) + it('works with zero length', function() + local screen = Screen.new(40, 5) + screen:attach() + funcs.setline(1, 'abcdef') + command('hi PreProc guifg=Red') + eq(4, funcs.matchaddpos('PreProc', {{1, 2, 0}}, 3, 4)) + eq({{ + group='PreProc', + pos1 = {1, 2, 0}, + priority=3, + id=4, + }}, funcs.getmatches()) + screen:expect([[ + ^a{1:b}cdef | + {2:~ }| + {2:~ }| + {2:~ }| + | + ]], {[1] = {foreground = Screen.colors.Red}, [2] = {bold = true, foreground = Screen.colors.Blue1}}) + end) +end) diff --git a/test/functional/eval/minmax_functions_spec.lua b/test/functional/eval/minmax_functions_spec.lua new file mode 100644 index 0000000000..c6eb754f91 --- /dev/null +++ b/test/functional/eval/minmax_functions_spec.lua @@ -0,0 +1,51 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local eval = helpers.eval +local clear = helpers.clear +local funcs = helpers.funcs +local redir_exec = helpers.redir_exec + +before_each(clear) +for _, func in ipairs({'min', 'max'}) do + describe(func .. '()', function() + it('gives a single error message when multiple values failed conversions', + function() + eq('\nE745: Using a List as a Number\n0', + redir_exec('echo ' .. func .. '([-5, [], [], [], 5])')) + eq('\nE745: Using a List as a Number\n0', + redir_exec('echo ' .. func .. '({1:-5, 2:[], 3:[], 4:[], 5:5})')) + for errmsg, errinput in pairs({ + ['E745: Using a List as a Number'] = '[]', + ['E805: Using a Float as a Number'] = '0.0', + ['E703: Using a Funcref as a Number'] = 'function("tr")', + ['E728: Using a Dictionary as a Number'] = '{}', + }) do + eq('\n' .. errmsg .. '\n0', + redir_exec('echo ' .. func .. '([' .. errinput .. '])')) + eq('\n' .. errmsg .. '\n0', + redir_exec('echo ' .. func .. '({1:' .. errinput .. '})')) + end + end) + it('works with arrays/dictionaries with zero items', function() + eq(0, funcs[func]({})) + eq(0, eval(func .. '({})')) + end) + it('works with arrays/dictionaries with one item', function() + eq(5, funcs[func]({5})) + eq(5, funcs[func]({test=5})) + end) + it('works with NULL arrays/dictionaries', function() + eq(0, eval(func .. '(v:_null_list)')) + eq(0, eval(func .. '(v:_null_dict)')) + end) + it('errors out for invalid types', function() + for _, errinput in ipairs({'1', 'v:true', 'v:false', 'v:null', + 'function("tr")', '""'}) do + eq(('\nE712: Argument of %s() must be a List or Dictionary\n0'):format( + func), + redir_exec('echo ' .. func .. '(' .. errinput .. ')')) + end + end) + end) +end diff --git a/test/functional/eval/modeline_spec.lua b/test/functional/eval/modeline_spec.lua new file mode 100644 index 0000000000..c5bb798f4a --- /dev/null +++ b/test/functional/eval/modeline_spec.lua @@ -0,0 +1,19 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear, command, write_file = helpers.clear, helpers.command, helpers.write_file +local eq, eval = helpers.eq, helpers.eval + +describe("modeline", function() + local tempfile = helpers.tmpname() + before_each(clear) + + after_each(function() + os.remove(tempfile) + end) + + it('does not crash with a large version number', function() + write_file(tempfile, 'vim100000000000000000000000') + command('e! ' .. tempfile) + + eq(2, eval('1+1')) -- Still alive? + end) +end) diff --git a/test/functional/eval/msgpack_functions_spec.lua b/test/functional/eval/msgpack_functions_spec.lua index 5b87b05652..a8a413f68b 100644 --- a/test/functional/eval/msgpack_functions_spec.lua +++ b/test/functional/eval/msgpack_functions_spec.lua @@ -2,7 +2,7 @@ local helpers = require('test.functional.helpers')(after_each) local clear = helpers.clear local funcs = helpers.funcs local eval, eq = helpers.eval, helpers.eq -local execute = helpers.execute +local command = helpers.command local nvim = helpers.nvim local exc_exec = helpers.exc_exec @@ -16,18 +16,18 @@ describe('msgpack*() functions', function() end) end - -- Regression test: msgpack_list_write was failing to write buffer with zero + -- Regression test: msgpack_list_write was failing to write buffer with zero -- length. obj_test('are able to dump and restore {"file": ""}', {{file=''}}) - -- Regression test: msgpack_list_write was failing to write buffer with NL at + -- Regression test: msgpack_list_write was failing to write buffer with NL at -- the end. obj_test('are able to dump and restore {0, "echo mpack"}', {{0, 'echo mpack'}}) obj_test('are able to dump and restore "Test\\n"', {'Test\n'}) - -- Regression test: msgpack_list_write was failing to write buffer with NL + -- Regression test: msgpack_list_write was failing to write buffer with NL -- inside. obj_test('are able to dump and restore "Test\\nTest 2"', {'Test\nTest 2'}) - -- Test that big objects (requirement: dump to something that is bigger then - -- IOSIZE) are also fine. This particular object is obtained by concatenating + -- Test that big objects (requirement: dump to something that is bigger then + -- IOSIZE) are also fine. This particular object is obtained by concatenating -- 5 identical shada files. local big_obj = { 1, 1436711454, 78, { @@ -331,13 +331,14 @@ describe('msgpack*() functions', function() obj_test('are able to dump and restore floating-point value', {0.125}) it('can restore and dump UINT64_MAX', function() - execute('let dumped = ["\\xCF" . repeat("\\xFF", 8)]') - execute('let parsed = msgpackparse(dumped)') - execute('let dumped2 = msgpackdump(parsed)') + command('let dumped = ["\\xCF" . repeat("\\xFF", 8)]') + command('let parsed = msgpackparse(dumped)') + command('let dumped2 = msgpackdump(parsed)') eq(1, eval('type(parsed[0]) == type(0) ' .. '|| parsed[0]._TYPE is v:msgpack_types.integer')) if eval('type(parsed[0]) == type(0)') == 1 then - eq(1, eval('0xFFFFFFFFFFFFFFFF == parsed[0]')) + command('call assert_equal(0xFFFFFFFFFFFFFFFF, parsed[0])') + eq({}, eval('v:errors')) else eq({_TYPE={}, _VAL={1, 3, 0x7FFFFFFF, 0x7FFFFFFF}}, eval('parsed[0]')) end @@ -345,13 +346,14 @@ describe('msgpack*() functions', function() end) it('can restore and dump INT64_MIN', function() - execute('let dumped = ["\\xD3\\x80" . repeat("\\n", 7)]') - execute('let parsed = msgpackparse(dumped)') - execute('let dumped2 = msgpackdump(parsed)') + command('let dumped = ["\\xD3\\x80" . repeat("\\n", 7)]') + command('let parsed = msgpackparse(dumped)') + command('let dumped2 = msgpackdump(parsed)') eq(1, eval('type(parsed[0]) == type(0) ' .. '|| parsed[0]._TYPE is v:msgpack_types.integer')) if eval('type(parsed[0]) == type(0)') == 1 then - eq(1, eval('-0x8000000000000000 == parsed[0]')) + command('call assert_equal(-0x7fffffffffffffff - 1, parsed[0])') + eq({}, eval('v:errors')) else eq({_TYPE={}, _VAL={-1, 2, 0, 0}}, eval('parsed[0]')) end @@ -359,33 +361,33 @@ describe('msgpack*() functions', function() end) it('can restore and dump BIN string with zero byte', function() - execute('let dumped = ["\\xC4\\x01\\n"]') - execute('let parsed = msgpackparse(dumped)') - execute('let dumped2 = msgpackdump(parsed)') + command('let dumped = ["\\xC4\\x01\\n"]') + command('let parsed = msgpackparse(dumped)') + command('let dumped2 = msgpackdump(parsed)') eq({{_TYPE={}, _VAL={'\n'}}}, eval('parsed')) eq(1, eval('parsed[0]._TYPE is v:msgpack_types.binary')) eq(1, eval('dumped ==# dumped2')) end) it('can restore and dump STR string with zero byte', function() - execute('let dumped = ["\\xA1\\n"]') - execute('let parsed = msgpackparse(dumped)') - execute('let dumped2 = msgpackdump(parsed)') + command('let dumped = ["\\xA1\\n"]') + command('let parsed = msgpackparse(dumped)') + command('let dumped2 = msgpackdump(parsed)') eq({{_TYPE={}, _VAL={'\n'}}}, eval('parsed')) eq(1, eval('parsed[0]._TYPE is v:msgpack_types.string')) eq(1, eval('dumped ==# dumped2')) end) it('can restore and dump BIN string with NL', function() - execute('let dumped = ["\\xC4\\x01", ""]') - execute('let parsed = msgpackparse(dumped)') - execute('let dumped2 = msgpackdump(parsed)') + command('let dumped = ["\\xC4\\x01", ""]') + command('let parsed = msgpackparse(dumped)') + command('let dumped2 = msgpackdump(parsed)') eq({"\n"}, eval('parsed')) eq(1, eval('dumped ==# dumped2')) end) it('dump and restore special mapping with floating-point value', function() - execute('let todump = {"_TYPE": v:msgpack_types.float, "_VAL": 0.125}') + command('let todump = {"_TYPE": v:msgpack_types.float, "_VAL": 0.125}') eq({0.125}, eval('msgpackparse(msgpackdump([todump]))')) end) end) @@ -394,52 +396,53 @@ describe('msgpackparse() function', function() before_each(clear) it('restores nil as v:null', function() - execute('let dumped = ["\\xC0"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\xC0"]') + command('let parsed = msgpackparse(dumped)') eq('[v:null]', eval('string(parsed)')) end) it('restores boolean false as v:false', function() - execute('let dumped = ["\\xC2"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\xC2"]') + command('let parsed = msgpackparse(dumped)') eq({false}, eval('parsed')) end) it('restores boolean true as v:true', function() - execute('let dumped = ["\\xC3"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\xC3"]') + command('let parsed = msgpackparse(dumped)') eq({true}, eval('parsed')) end) it('restores FIXSTR as special dict', function() - execute('let dumped = ["\\xa2ab"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\xa2ab"]') + command('let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={'ab'}}}, eval('parsed')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.string')) end) it('restores BIN 8 as string', function() - execute('let dumped = ["\\xC4\\x02ab"]') + command('let dumped = ["\\xC4\\x02ab"]') eq({'ab'}, eval('msgpackparse(dumped)')) end) it('restores FIXEXT1 as special dictionary', function() - execute('let dumped = ["\\xD4\\x10", ""]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\xD4\\x10", ""]') + command('let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, eval('parsed')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.ext')) end) it('restores MAP with BIN key as special dictionary', function() - execute('let dumped = ["\\x81\\xC4\\x01a\\xC4\\n"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\x81\\xC4\\x01a\\xC4\\n"]') + command('let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={{'a', ''}}}}, eval('parsed')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) end) it('restores MAP with duplicate STR keys as special dictionary', function() - execute('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]') + -- FIXME Internal error bug + command('silent! let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={ {{_TYPE={}, _VAL={'a'}}, ''}, {{_TYPE={}, _VAL={'a'}}, ''}}} }, eval('parsed')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) @@ -448,8 +451,8 @@ describe('msgpackparse() function', function() end) it('restores MAP with MAP key as special dictionary', function() - execute('let dumped = ["\\x81\\x80\\xC4\\n"]') - execute('let parsed = msgpackparse(dumped)') + command('let dumped = ["\\x81\\x80\\xC4\\n"]') + command('let parsed = msgpackparse(dumped)') eq({{_TYPE={}, _VAL={{{}, ''}}}}, eval('parsed')) eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) end) @@ -460,7 +463,8 @@ describe('msgpackparse() function', function() eval(cmd) eval(cmd) -- do it again (try to force segfault) local api_info = eval(cmd) -- do it again - eq({'error_types', 'functions', 'types'}, api_info) + eq({'error_types', 'functions', 'types', + 'ui_events', 'ui_options', 'version'}, api_info) end) it('fails when called with no arguments', function() @@ -493,6 +497,12 @@ describe('msgpackparse() function', function() exc_exec('call msgpackparse(function("tr"))')) end) + it('fails to parse a partial', function() + command('function T() dict\nendfunction') + eq('Vim(call):E686: Argument of msgpackparse() must be a List', + exc_exec('call msgpackparse(function("T", [1, 2], {}))')) + end) + it('fails to parse a float', function() eq('Vim(call):E686: Argument of msgpackparse() must be a List', exc_exec('call msgpackparse(0.0)')) @@ -508,10 +518,10 @@ describe('msgpackdump() function', function() end) it('can dump generic mapping with generic mapping keys and values', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('call add(todump._VAL, [todumpv1, todumpv2])') + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('call add(todump._VAL, [todumpv1, todumpv2])') eq({'\129\128\128'}, eval('msgpackdump([todump])')) end) @@ -524,124 +534,131 @@ describe('msgpackdump() function', function() end) it('can v:null', function() - execute('let todump = v:null') + command('let todump = v:null') end) it('can dump special bool mapping (true)', function() - execute('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 1}') + command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 1}') eq({'\195'}, eval('msgpackdump([todump])')) end) it('can dump special bool mapping (false)', function() - execute('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 0}') + command('let todump = {"_TYPE": v:msgpack_types.boolean, "_VAL": 0}') eq({'\194'}, eval('msgpackdump([todump])')) end) it('can dump special nil mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.nil, "_VAL": 0}') + command('let todump = {"_TYPE": v:msgpack_types.nil, "_VAL": 0}') eq({'\192'}, eval('msgpackdump([todump])')) end) it('can dump special ext mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') + command('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') eq({'\212\005', ''}, eval('msgpackdump([todump])')) end) it('can dump special array mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') eq({'\146\005\145\196\n'}, eval('msgpackdump([todump])')) end) it('can dump special UINT64_MAX mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.integer}') - execute('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') + command('let todump = {"_TYPE": v:msgpack_types.integer}') + command('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') eq({'\207\255\255\255\255\255\255\255\255'}, eval('msgpackdump([todump])')) end) it('can dump special INT64_MIN mapping', function() - execute('let todump = {"_TYPE": v:msgpack_types.integer}') - execute('let todump._VAL = [-1, 2, 0, 0]') + command('let todump = {"_TYPE": v:msgpack_types.integer}') + command('let todump._VAL = [-1, 2, 0, 0]') eq({'\211\128\n\n\n\n\n\n\n'}, eval('msgpackdump([todump])')) end) it('fails to dump a function reference', function() - execute('let Todump = function("tr")') - eq('Vim(call):E951: Error while dumping msgpackdump() argument, index 0, itself: attempt to dump function reference', + command('let Todump = function("tr")') + eq('Vim(call):E5004: Error while dumping msgpackdump() argument, index 0, itself: attempt to dump function reference', + exc_exec('call msgpackdump([Todump])')) + end) + + it('fails to dump a partial', function() + command('function T() dict\nendfunction') + command('let Todump = function("T", [1, 2], {})') + eq('Vim(call):E5004: Error while dumping msgpackdump() argument, index 0, itself: attempt to dump function reference', exc_exec('call msgpackdump([Todump])')) end) it('fails to dump a function reference in a list', function() - execute('let todump = [function("tr")]') - eq('Vim(call):E951: Error while dumping msgpackdump() argument, index 0, index 0: attempt to dump function reference', + command('let todump = [function("tr")]') + eq('Vim(call):E5004: Error while dumping msgpackdump() argument, index 0, index 0: attempt to dump function reference', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive list', function() - execute('let todump = [[[]]]') - execute('call add(todump[0][0], todump)') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in index 0, index 0, index 0', + command('let todump = [[[]]]') + command('call add(todump[0][0], todump)') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in index 0, index 0, index 0', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive dict', function() - execute('let todump = {"d": {"d": {}}}') - execute('call extend(todump.d.d, {"d": todump})') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in key \'d\', key \'d\', key \'d\'', + command('let todump = {"d": {"d": {}}}') + command('call extend(todump.d.d, {"d": todump})') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in key \'d\', key \'d\', key \'d\'', exc_exec('call msgpackdump([todump])')) end) it('can dump dict with two same dicts inside', function() - execute('let inter = {}') - execute('let todump = {"a": inter, "b": inter}') + command('let inter = {}') + command('let todump = {"a": inter, "b": inter}') eq({"\130\161a\128\161b\128"}, eval('msgpackdump([todump])')) end) it('can dump list with two same lists inside', function() - execute('let inter = []') - execute('let todump = [inter, inter]') + command('let inter = []') + command('let todump = [inter, inter]') eq({"\146\144\144"}, eval('msgpackdump([todump])')) end) it('fails to dump a recursive list in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') - execute('call add(todump._VAL, todump)') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in index 0', + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + command('call add(todump._VAL, todump)') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in index 0', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive (key) map in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('call add(todump._VAL, [todump, 0])') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in index 1', + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('call add(todump._VAL, [todump, 0])') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in index 0', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive (val) map in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') - execute('call add(todump._VAL, [0, todump])') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in key 0 at index 0 from special map', + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + command('call add(todump._VAL, [0, todump])') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in key 0 at index 0 from special map', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive (key) map in a special dict, _VAL reference', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[[], []]]}') - execute('call add(todump._VAL[0][0], todump._VAL)') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in key [[[[...@0], []]]] at index 0 from special map, index 0', + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[[], []]]}') + command('call add(todump._VAL[0][0], todump._VAL)') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in key [[[[...@0], []]]] at index 0 from special map, index 0', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive (val) map in a special dict, _VAL reference', function() - execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[[], []]]}') - execute('call add(todump._VAL[0][1], todump._VAL)') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in key [] at index 0 from special map, index 0', + command('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": [[[], []]]}') + command('call add(todump._VAL[0][1], todump._VAL)') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in key [] at index 0 from special map, index 0', exc_exec('call msgpackdump([todump])')) end) it('fails to dump a recursive (val) special list in a special dict', function() - execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') - execute('call add(todump._VAL, [0, todump._VAL])') - eq('Vim(call):E952: Unable to dump msgpackdump() argument, index 0: container references itself in index 0, index 1', + command('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + command('call add(todump._VAL, [0, todump._VAL])') + eq('Vim(call):E5005: Unable to dump msgpackdump() argument, index 0: container references itself in index 0, index 1', exc_exec('call msgpackdump([todump])')) end) @@ -675,6 +692,12 @@ describe('msgpackdump() function', function() exc_exec('call msgpackdump(function("tr"))')) end) + it('fails to dump a partial', function() + command('function T() dict\nendfunction') + eq('Vim(call):E686: Argument of msgpackdump() must be a List', + exc_exec('call msgpackdump(function("T", [1, 2], {}))')) + end) + it('fails to dump a float', function() eq('Vim(call):E686: Argument of msgpackdump() must be a List', exc_exec('call msgpackdump(0.0)')) diff --git a/test/functional/eval/null_spec.lua b/test/functional/eval/null_spec.lua new file mode 100644 index 0000000000..afe999e1fa --- /dev/null +++ b/test/functional/eval/null_spec.lua @@ -0,0 +1,147 @@ +local helpers = require('test.functional.helpers')(after_each) + +local curbufmeths = helpers.curbufmeths +local redir_exec = helpers.redir_exec +local exc_exec = helpers.exc_exec +local command = helpers.command +local clear = helpers.clear +local meths = helpers.meths +local funcs = helpers.funcs +local eq = helpers.eq + +describe('NULL', function() + before_each(function() + clear() + command('let L = v:_null_list') + command('let D = v:_null_dict') + command('let S = $XXX_NONEXISTENT_VAR_XXX') + end) + local tmpfname = 'Xtest-functional-viml-null' + after_each(function() + os.remove(tmpfname) + end) + local null_test = function(name, cmd, err) + it(name, function() + eq(err, exc_exec(cmd)) + end) + end + local null_expr_test = function(name, expr, err, val, after) + it(name, function() + eq((err == 0) and ('') or ('\n' .. err), + redir_exec('let g:_var = ' .. expr)) + if val == nil then + eq(0, funcs.exists('g:_var')) + else + eq(val, meths.get_var('_var')) + end + if after ~= nil then + after() + end + end) + end + describe('list', function() + -- Incorrect behaviour + -- FIXME Should error out with different message + null_test('makes :unlet act as if it is not a list', ':unlet L[0]', + 'Vim(unlet):E689: Can only index a List or Dictionary') + + -- Subjectable behaviour + + -- FIXME Should return 1 + null_expr_test('is equal to empty list', 'L == []', 0, 0) + -- FIXME Should return 1 + null_expr_test('is equal to empty list (reverse order)', '[] == L', 0, 0) + + -- Correct behaviour + null_expr_test('can be indexed with error message for empty list', 'L[0]', + 'E684: list index out of range: 0\nE15: Invalid expression: L[0]', nil) + null_expr_test('can be splice-indexed', 'L[:]', 0, {}) + null_expr_test('is not locked', 'islocked("v:_null_list")', 0, 0) + null_test('is accepted by :for', 'for x in L|throw x|endfor', 0) + null_expr_test('does not crash append()', 'append(1, L)', 0, 0, function() + eq({''}, curbufmeths.get_lines(0, -1, false)) + end) + null_expr_test('does not crash setline()', 'setline(1, L)', 0, 0, function() + eq({''}, curbufmeths.get_lines(0, -1, false)) + end) + null_expr_test('is identical to itself', 'L is L', 0, 1) + null_expr_test('can be sliced', 'L[:]', 0, {}) + null_expr_test('can be copied', 'copy(L)', 0, {}) + null_expr_test('can be deepcopied', 'deepcopy(L)', 0, {}) + null_expr_test('does not crash when indexed', 'L[1]', + 'E684: list index out of range: 1\nE15: Invalid expression: L[1]', nil) + null_expr_test('does not crash call()', 'call("arglistid", L)', 0, 0) + null_expr_test('does not crash col()', 'col(L)', 0, 0) + null_expr_test('does not crash virtcol()', 'virtcol(L)', 0, 0) + null_expr_test('does not crash line()', 'line(L)', 0, 0) + null_expr_test('does not crash count()', 'count(L, 1)', 0, 0) + null_expr_test('does not crash cursor()', 'cursor(L)', 'E474: Invalid argument', -1) + null_expr_test('does not crash map()', 'map(L, "v:val")', 0, {}) + null_expr_test('does not crash filter()', 'filter(L, "1")', 0, {}) + null_expr_test('is empty', 'empty(L)', 0, 1) + null_expr_test('does not crash get()', 'get(L, 1, 10)', 0, 10) + null_expr_test('has zero length', 'len(L)', 0, 0) + null_expr_test('is accepted as an empty list by max()', 'max(L)', 0, 0) + null_expr_test('is accepted as an empty list by min()', 'min(L)', 0, 0) + null_expr_test('is stringified correctly', 'string(L)', 0, '[]') + null_expr_test('is JSON encoded correctly', 'json_encode(L)', 0, '[]') + null_test('does not crash lockvar', 'lockvar! L', 0) + null_expr_test('can be added to itself', '(L + L)', 0, {}) + null_expr_test('can be added to itself', '(L + L) is L', 0, 1) + null_expr_test('can be added to non-empty list', '([1] + L)', 0, {1}) + null_expr_test('can be added to non-empty list (reversed)', '(L + [1])', 0, {1}) + null_expr_test('is equal to itself', 'L == L', 0, 1) + null_expr_test('is not not equal to itself', 'L != L', 0, 0) + null_expr_test('counts correctly', 'count([L], L)', 0, 1) + null_expr_test('makes map() return v:_null_list', 'map(L, "v:val") is# L', 0, 1) + null_expr_test('makes filter() return v:_null_list', 'filter(L, "1") is# L', 0, 1) + null_test('is treated by :let as empty list', ':let [l] = L', 'Vim(let):E688: More targets than List items') + null_expr_test('is accepted as an empty list by inputlist()', '[feedkeys("\\n"), inputlist(L)]', + 'Type number and <Enter> or click with mouse (empty cancels): ', {0, 0}) + null_expr_test('is accepted as an empty list by writefile()', + ('[writefile(L, "%s"), readfile("%s")]'):format(tmpfname, tmpfname), + 0, {0, {}}) + null_expr_test('makes add() error out', 'add(L, 0)', + 'E742: Cannot change value of add() argument', 1) + null_expr_test('makes insert() error out', 'insert(L, 1)', + 'E742: Cannot change value of insert() argument', 0) + null_expr_test('does not crash remove()', 'remove(L, 0)', + 'E742: Cannot change value of remove() argument', 0) + null_expr_test('makes reverse() error out', 'reverse(L)', + 'E742: Cannot change value of reverse() argument', 0) + null_expr_test('makes sort() error out', 'sort(L)', + 'E742: Cannot change value of sort() argument', 0) + null_expr_test('makes uniq() error out', 'uniq(L)', + 'E742: Cannot change value of uniq() argument', 0) + null_expr_test('does not crash extend()', 'extend(L, [1])', 'E742: Cannot change value of extend() argument', 0) + null_expr_test('does not crash extend() (second position)', 'extend([1], L)', 0, {1}) + null_expr_test('makes join() return empty string', 'join(L, "")', 0, '') + null_expr_test('makes msgpackdump() return empty list', 'msgpackdump(L)', 0, {}) + null_expr_test('does not crash system()', 'system("cat", L)', 0, '') + null_expr_test('does not crash setreg', 'setreg("x", L)', 0, 0) + null_expr_test('does not crash systemlist()', 'systemlist("cat", L)', 0, {}) + null_test('does not make Neovim crash when v:oldfiles gets assigned to that', ':let v:oldfiles = L|oldfiles', 0) + null_expr_test('does not make complete() crash or error out', + 'execute(":normal i\\<C-r>=complete(1, L)[-1]\\n")', + '', '\n', function() + eq({''}, curbufmeths.get_lines(0, -1, false)) + end) + null_expr_test('is accepted by setmatches()', 'setmatches(L)', 0, 0) + null_expr_test('is accepted by setqflist()', 'setqflist(L)', 0, 0) + null_expr_test('is accepted by setloclist()', 'setloclist(1, L)', 0, 0) + null_test('is accepted by :cexpr', 'cexpr L', 0) + null_test('is accepted by :lexpr', 'lexpr L', 0) + end) + describe('dict', function() + it('does not crash when indexing NULL dict', function() + eq('\nE716: Key not present in Dictionary: test\nE15: Invalid expression: v:_null_dict.test', + redir_exec('echo v:_null_dict.test')) + end) + null_expr_test('makes extend error out', 'extend(D, {})', 'E742: Cannot change value of extend() argument', 0) + null_expr_test('makes extend do nothing', 'extend({1: 2}, D)', 0, {['1']=2}) + null_expr_test('does not crash map()', 'map(D, "v:val")', 0, {}) + null_expr_test('does not crash filter()', 'filter(D, "1")', 0, {}) + null_expr_test('makes map() return v:_null_dict', 'map(D, "v:val") is# D', 0, 1) + null_expr_test('makes filter() return v:_null_dict', 'filter(D, "1") is# D', 0, 1) + end) +end) diff --git a/test/functional/eval/printf_spec.lua b/test/functional/eval/printf_spec.lua index c84290ceef..27e24c4118 100644 --- a/test/functional/eval/printf_spec.lua +++ b/test/functional/eval/printf_spec.lua @@ -1,7 +1,10 @@ local helpers = require('test.functional.helpers')(after_each) + local clear = helpers.clear local eq = helpers.eq +local eval = helpers.eval local funcs = helpers.funcs +local meths = helpers.meths local exc_exec = helpers.exc_exec describe('printf()', function() @@ -57,4 +60,33 @@ describe('printf()', function() it('errors out when %b modifier is used for a float', function() eq('Vim(call):E805: Using a Float as a Number', exc_exec('call printf("%b", 3.1415926535)')) end) + it('works with %p correctly', function() + local null_ret = nil + local seen_rets = {} + -- Collect all args in an array to avoid possible allocation of the same + -- address after freeing unreferenced values. + meths.set_var('__args', {}) + local function check_printf(expr, is_null) + eq(0, exc_exec('call add(__args, ' .. expr .. ')')) + eq(0, exc_exec('let __result = printf("%p", __args[-1])')) + local id_ret = eval('id(__args[-1])') + eq(id_ret, meths.get_var('__result')) + if is_null then + if null_ret then + eq(null_ret, id_ret) + else + null_ret = id_ret + end + else + eq(nil, seen_rets[id_ret]) + seen_rets[id_ret] = expr + end + meths.del_var('__result') + end + check_printf('v:_null_list', true) + check_printf('v:_null_dict', true) + check_printf('[]') + check_printf('{}') + check_printf('function("tr", ["a"])') + end) end) diff --git a/test/functional/eval/reltime_spec.lua b/test/functional/eval/reltime_spec.lua index 0b19d372ec..0181f09024 100644 --- a/test/functional/eval/reltime_spec.lua +++ b/test/functional/eval/reltime_spec.lua @@ -1,6 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) local clear, eq, ok = helpers.clear, helpers.eq, helpers.ok -local neq, execute, funcs = helpers.neq, helpers.execute, helpers.funcs +local neq, command, funcs = helpers.neq, helpers.command, helpers.funcs local reltime, reltimestr, reltimefloat = funcs.reltime, funcs.reltimestr, funcs.reltimefloat describe('reltimestr(), reltimefloat()', function() @@ -8,7 +8,7 @@ describe('reltimestr(), reltimefloat()', function() it('Checks', function() local now = reltime() - execute('sleep 10m') + command('sleep 10m') local later = reltime() local elapsed = reltime(now) diff --git a/test/functional/eval/server_spec.lua b/test/functional/eval/server_spec.lua index 0fd55ce2f9..563e619b39 100644 --- a/test/functional/eval/server_spec.lua +++ b/test/functional/eval/server_spec.lua @@ -1,24 +1,41 @@ - local helpers = require('test.functional.helpers')(after_each) -local nvim, eq, neq, eval = helpers.nvim, helpers.eq, helpers.neq, helpers.eval +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 - -describe('serverstart(), serverstop()', function() +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 +end + +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 - nvim('command', 'let $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')) - nvim('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, @@ -31,39 +48,75 @@ 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-server-pipe]] - or 'Xtest-functional-server-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() - nvim('command', "call serverstop('')") - nvim('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) -end) + it('parses endpoints correctly', function() + clear_serverlist() + eq({}, funcs.serverlist()) -describe('serverlist()', function() - before_each(clear) + local s = funcs.serverstart('127.0.0.1:0') -- assign random port + if #s > 0 then + assert(string.match(s, '127.0.0.1:%d+')) + eq(s, funcs.serverlist()[1]) + clear_serverlist() + end + + s = funcs.serverstart('127.0.0.1:') -- assign random port + if #s > 0 then + assert(string.match(s, '127.0.0.1:%d+')) + eq(s, funcs.serverlist()[1]) + clear_serverlist() + end + + local expected = {} + local v4 = '127.0.0.1:12345' + local status, _ = pcall(funcs.serverstart, v4) + if status then + table.insert(expected, v4) + pcall(funcs.serverstart, v4) -- exists already; ignore + end - it('returns the list of servers', function() + local v6 = '::1:12345' + status, _ = pcall(funcs.serverstart, v6) + if status then + table.insert(expected, v6) + pcall(funcs.serverstart, v6) -- exists already; ignore + end + eq(expected, funcs.serverlist()) + clear_serverlist() + + expect_err('Failed to start server: invalid argument', + funcs.serverstart, '127.0.0.1:65536') -- invalid port + eq({}, funcs.serverlist()) + end) + + 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 = {'should-not-exist', 'another-one-that-shouldnt'} + -- Add some servers. + local servs = (iswin() + and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] } + or { [[Xtest-pipe0934]], [[Xtest-pipe4324]] }) for _, s in ipairs(servs) do - eq(s, eval('serverstart("'..s..'")')) + eq(s, eval("serverstart('"..s.."')")) end local new_servs = eval('serverlist()') @@ -73,10 +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]) - nvim('command', 'call serverstop("'..servs[i]..'")') + eq(1, eval("serverstop('"..servs[i].."')")) end - -- After calling serverstop() on the new servers, they should no longer be - -- in the list. + -- 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/setpos_spec.lua b/test/functional/eval/setpos_spec.lua new file mode 100644 index 0000000000..935f387bcc --- /dev/null +++ b/test/functional/eval/setpos_spec.lua @@ -0,0 +1,64 @@ +local helpers = require('test.functional.helpers')(after_each) +local setpos = helpers.funcs.setpos +local getpos = helpers.funcs.getpos +local insert = helpers.insert +local clear = helpers.clear +local command = helpers.command +local eval = helpers.eval +local eq = helpers.eq +local exc_exec = helpers.exc_exec + + +describe('setpos() function', function() + before_each(function() + clear() + insert([[ + First line of text + Second line of text + Third line of text]]) + command('new') + insert([[ + Line of text 1 + Line of text 2 + Line of text 3]]) + end) + it('can set the current cursor position', function() + setpos(".", {0, 2, 1, 0}) + eq(getpos("."), {0, 2, 1, 0}) + setpos(".", {2, 1, 1, 0}) + eq(getpos("."), {0, 1, 1, 0}) + local ret = exc_exec('call setpos(".", [1, 1, 1, 0])') + eq(0, ret) + end) + it('can set lowercase marks in the current buffer', function() + setpos("'d", {0, 2, 1, 0}) + eq(getpos("'d"), {0, 2, 1, 0}) + command('undo') + command('call setpos("\'d", [2, 3, 1, 0])') + eq(getpos("'d"), {0, 3, 1, 0}) + end) + it('can set lowercase marks in other buffers', function() + local retval = setpos("'d", {1, 2, 1, 0}) + eq(0, retval) + setpos("'d", {1, 2, 1, 0}) + eq(getpos("'d"), {0, 0, 0, 0}) + command('wincmd w') + eq(eval('bufnr("%")'), 1) + eq(getpos("'d"), {0, 2, 1, 0}) + end) + it("fails when setting a mark in a buffer that doesn't exist", function() + local retval = setpos("'d", {3, 2, 1, 0}) + eq(-1, retval) + eq(getpos("'d"), {0, 0, 0, 0}) + retval = setpos("'D", {3, 2, 1, 0}) + eq(-1, retval) + eq(getpos("'D"), {0, 0, 0, 0}) + end) + it('can set uppercase marks', function() + setpos("'D", {2, 2, 3, 0}) + eq(getpos("'D"), {2, 2, 3, 0}) + -- Can set a mark in another buffer + setpos("'D", {1, 2, 2, 0}) + eq(getpos("'D"), {1, 2, 2, 0}) + end) +end) diff --git a/test/functional/eval/sort_spec.lua b/test/functional/eval/sort_spec.lua new file mode 100644 index 0000000000..82557575ce --- /dev/null +++ b/test/functional/eval/sort_spec.lua @@ -0,0 +1,56 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local NIL = helpers.NIL +local eval = helpers.eval +local clear = helpers.clear +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) + +describe('sort()', function() + it('errors out when sorting special values', function() + eq('Vim(call):E907: Using a special value as a Float', + exc_exec('call sort([v:true, v:false], "f")')) + end) + + it('sorts “wrong” values between -0.0001 and 0.0001, preserving order', + function() + meths.set_var('list', {true, false, NIL, {}, {a=42}, 'check', + 0.0001, -0.0001}) + command('call insert(g:list, function("tr"))') + local error_lines = funcs.split( + funcs.execute('silent! call sort(g:list, "f")'), '\n') + local errors = {} + for _, err in ipairs(error_lines) do + errors[err] = true + end + eq({ + ['E891: Using a Funcref as a Float']=true, + ['E892: Using a String as a Float']=true, + ['E893: Using a List as a Float']=true, + ['E894: Using a Dictionary as a Float']=true, + ['E907: Using a special value as a Float']=true, + }, errors) + 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/special_vars_spec.lua b/test/functional/eval/special_vars_spec.lua index 4c5d63ce23..97a12d490d 100644 --- a/test/functional/eval/special_vars_spec.lua +++ b/test/functional/eval/special_vars_spec.lua @@ -1,6 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) local exc_exec = helpers.exc_exec -local execute = helpers.execute +local command = helpers.command local funcs = helpers.funcs local clear = helpers.clear local eval = helpers.eval @@ -12,7 +12,7 @@ describe('Special values', function() before_each(clear) it('do not cause error when freed', function() - execute([[ + command([[ function Test() try return v:true @@ -109,7 +109,7 @@ describe('Special values', function() it('does not work with +=/-=/.=', function() meths.set_var('true', true) meths.set_var('false', false) - execute('let null = v:null') + command('let null = v:null') eq('Vim(let):E734: Wrong variable type for +=', exc_exec('let true += 1')) eq('Vim(let):E734: Wrong variable type for +=', exc_exec('let false += 1')) @@ -168,4 +168,23 @@ describe('Special values', function() 'Expected True but got v:null', }, meths.get_vvar('errors')) end) + + describe('compat', function() + it('v:count is distinct from count', function() + command('let count = []') -- v:count is readonly + eq(1, eval('count is# g:["count"]')) + end) + it('v:errmsg is distinct from errmsg', function() + command('let errmsg = 1') + eq(1, eval('errmsg is# g:["errmsg"]')) + end) + it('v:shell_error is distinct from shell_error', function() + command('let shell_error = []') -- v:shell_error is readonly + eq(1, eval('shell_error is# g:["shell_error"]')) + end) + it('v:this_session is distinct from this_session', function() + command('let this_session = []') + eq(1, eval('this_session is# g:["this_session"]')) + end) + end) end) diff --git a/test/functional/eval/string_spec.lua b/test/functional/eval/string_spec.lua index 9e2dc4e111..adc1af9b8e 100644 --- a/test/functional/eval/string_spec.lua +++ b/test/functional/eval/string_spec.lua @@ -7,8 +7,9 @@ local eval = helpers.eval local exc_exec = helpers.exc_exec local redir_exec = helpers.redir_exec local funcs = helpers.funcs -local write_file = helpers.write_file local NIL = helpers.NIL +local source = helpers.source +local dedent = helpers.dedent describe('string() function', function() before_each(clear) @@ -103,26 +104,19 @@ describe('string() function', function() end) describe('used to represent funcrefs', function() - local fname = 'Xtest-functional-eval-string_spec-fref-script.vim' - before_each(function() - write_file(fname, [[ + source([[ function Test1() endfunction - function s:Test2() + function s:Test2() dict endfunction - function g:Test3() + function g:Test3() dict endfunction let g:Test2_f = function('s:Test2') ]]) - command('source ' .. fname) - end) - - after_each(function() - os.remove(fname) end) it('dumps references to built-in functions', function() @@ -137,6 +131,85 @@ describe('string() function', function() it('dumps references to script functions', function() eq('function(\'<SNR>1_Test2\')', eval('string(Test2_f)')) end) + + it('dumps partials with self referencing a partial', function() + source([[ + function TestDict() dict + endfunction + let d = {} + let TestDictRef = function('TestDict', d) + let d.tdr = TestDictRef + ]]) + eq("\nE724: unable to correctly dump variable with self-referencing container\nfunction('TestDict', {'tdr': function('TestDict', {E724@1})})", + redir_exec('echo string(d.tdr)')) + end) + + it('dumps automatically created partials', function() + eq('function(\'<SNR>1_Test2\', {\'f\': function(\'<SNR>1_Test2\')})', + eval('string({"f": Test2_f}.f)')) + eq('function(\'<SNR>1_Test2\', [1], {\'f\': function(\'<SNR>1_Test2\', [1])})', + eval('string({"f": function(Test2_f, [1])}.f)')) + end) + + it('dumps manually created partials', function() + eq('function(\'Test3\', [1, 2], {})', + eval('string(function("Test3", [1, 2], {}))')) + eq('function(\'Test3\', {})', + eval('string(function("Test3", {}))')) + eq('function(\'Test3\', [1, 2])', + eval('string(function("Test3", [1, 2]))')) + end) + + it('does not crash or halt when dumping partials with reference cycles in self', + function() + meths.set_var('d', {v=true}) + eq(dedent([[ + + E724: unable to correctly dump variable with self-referencing container + {'p': function('<SNR>1_Test2', {E724@0}), 'f': function('<SNR>1_Test2'), 'v': v:true}]]), + redir_exec('echo string(extend(extend(g:d, {"f": g:Test2_f}), {"p": g:d.f}))')) + end) + + it('does not show errors when dumping partials referencing the same dictionary', + function() + command('let d = {}') + -- Regression for “eval/typval_encode: Dump empty dictionary before + -- checking for refcycle”, results in error. + eq('[function(\'tr\', {}), function(\'tr\', {})]', eval('string([function("tr", d), function("tr", d)])')) + -- Regression for “eval: Work with reference cycles in partials (self) + -- properly”, results in crash. + eval('extend(d, {"a": 1})') + eq('[function(\'tr\', {\'a\': 1}), function(\'tr\', {\'a\': 1})]', eval('string([function("tr", d), function("tr", d)])')) + end) + + it('does not crash or halt when dumping partials with reference cycles in arguments', + function() + meths.set_var('l', {}) + eval('add(l, l)') + -- Regression: the below line used to crash (add returns original list and + -- there was error in dumping partials). Tested explicitly in + -- test/unit/api/private_helpers_spec.lua. + eval('add(l, function("Test1", l))') + eq(dedent([=[ + + E724: unable to correctly dump variable with self-referencing container + function('Test1', [[{E724@2}, function('Test1', [{E724@2}])], function('Test1', [[{E724@4}, function('Test1', [{E724@4}])]])])]=]), + redir_exec('echo string(function("Test1", l))')) + end) + + it('does not crash or halt when dumping partials with reference cycles in self and arguments', + function() + meths.set_var('d', {v=true}) + meths.set_var('l', {}) + eval('add(l, l)') + eval('add(l, function("Test1", l))') + eval('add(l, function("Test1", d))') + eq(dedent([=[ + + E724: unable to correctly dump variable with self-referencing container + {'p': function('<SNR>1_Test2', [[{E724@3}, function('Test1', [{E724@3}]), function('Test1', {E724@0})], function('Test1', [[{E724@5}, function('Test1', [{E724@5}]), function('Test1', {E724@0})]]), function('Test1', {E724@0})], {E724@0}), 'f': function('<SNR>1_Test2'), 'v': v:true}]=]), + redir_exec('echo string(extend(extend(g:d, {"f": g:Test2_f}), {"p": function(g:d.f, l)}))')) + end) end) describe('used to represent lists', function() @@ -174,6 +247,13 @@ describe('string() function', function() eq('{}', eval('string({})')) end) + it('dumps list with two same empty dictionaries, also in partials', function() + command('let d = {}') + eq('[{}, {}]', eval('string([d, d])')) + eq('[function(\'tr\', {}), {}]', eval('string([function("tr", d), d])')) + eq('[{}, function(\'tr\', {})]', eval('string([d, function("tr", d)])')) + end) + it('dumps non-empty dictionary', function() eq('{\'t\'\'est\': 1}', funcs.string({['t\'est']=1})) end) diff --git a/test/functional/eval/system_spec.lua b/test/functional/eval/system_spec.lua new file mode 100644 index 0000000000..5cbf34365b --- /dev/null +++ b/test/functional/eval/system_spec.lua @@ -0,0 +1,574 @@ +local helpers = require('test.functional.helpers')(after_each) + +local nvim_dir = helpers.nvim_dir +local eq, call, clear, eval, feed_command, feed, nvim = + helpers.eq, helpers.call, helpers.clear, helpers.eval, helpers.feed_command, + helpers.feed, helpers.nvim +local command = helpers.command +local exc_exec = helpers.exc_exec +local iswin = helpers.iswin + +local Screen = require('test.functional.ui.screen') + +local function create_file_with_nuls(name) + return function() + feed('ipart1<C-V>000part2<C-V>000part3<ESC>:w '..name..'<CR>') + eval('1') -- wait for the file to be created + end +end + +local function delete_file(name) + return function() + eval("delete('"..name.."')") + end +end + +-- Some tests require the xclip program and a x server. +local xclip = nil +do + if os.getenv('DISPLAY') then + xclip = (os.execute('command -v xclip > /dev/null 2>&1') == 0) + end +end + +describe('system()', function() + before_each(clear) + + describe('command passed as a List', function() + local function printargs_path() + return nvim_dir..'/printargs-test' .. (iswin() and '.exe' or '') + end + + it('sets v:shell_error if cmd[0] is not executable', function() + call('system', { 'this-should-not-exist' }) + eq(-1, eval('v:shell_error')) + end) + + it('parameter validation does NOT modify v:shell_error', function() + -- 1. Call system() with invalid parameters. + -- 2. Assert that v:shell_error was NOT set. + feed_command('call system({})') + eq('E475: Invalid argument: expected String or List', eval('v:errmsg')) + eq(0, eval('v:shell_error')) + feed_command('call system([])') + eq('E474: Invalid argument', eval('v:errmsg')) + eq(0, eval('v:shell_error')) + + -- Provoke a non-zero v:shell_error. + call('system', { 'this-should-not-exist' }) + local old_val = eval('v:shell_error') + eq(-1, old_val) + + -- 1. Call system() with invalid parameters. + -- 2. Assert that v:shell_error was NOT modified. + feed_command('call system({})') + eq(old_val, eval('v:shell_error')) + feed_command('call system([])') + eq(old_val, eval('v:shell_error')) + end) + + it('quotes arguments correctly #5280', function() + local out = call('system', + { printargs_path(), [[1]], [[2 "3]], [[4 ' 5]], [[6 ' 7']] }) + + eq(0, eval('v:shell_error')) + eq([[arg1=1;arg2=2 "3;arg3=4 ' 5;arg4=6 ' 7';]], out) + + out = call('system', { printargs_path(), [['1]], [[2 "3]] }) + eq(0, eval('v:shell_error')) + eq([[arg1='1;arg2=2 "3;]], out) + + out = call('system', { printargs_path(), "A\nB" }) + eq(0, eval('v:shell_error')) + eq("arg1=A\nB;", out) + end) + + it('calls executable in $PATH', function() + if 0 == eval("executable('python')") then pending("missing `python`") end + eq("foo\n", eval([[system(['python', '-c', 'print("foo")'])]])) + eq(0, eval('v:shell_error')) + end) + + it('does NOT run in shell', function() + if iswin() then + eq("%PATH%\n", eval("system(['powershell', '-NoProfile', '-NoLogo', '-ExecutionPolicy', 'RemoteSigned', '-Command', 'echo', '%PATH%'])")) + else + eq("* $PATH %PATH%\n", eval("system(['echo', '*', '$PATH', '%PATH%'])")) + end + end) + end) + + it('sets v:shell_error', function() + if iswin() then + eval([[system("cmd.exe /c exit")]]) + eq(0, eval('v:shell_error')) + eval([[system("cmd.exe /c exit 1")]]) + eq(1, eval('v:shell_error')) + eval([[system("cmd.exe /c exit 5")]]) + eq(5, eval('v:shell_error')) + eval([[system('this-should-not-exist')]]) + eq(1, eval('v:shell_error')) + else + eval([[system("sh -c 'exit'")]]) + eq(0, eval('v:shell_error')) + eval([[system("sh -c 'exit 1'")]]) + eq(1, eval('v:shell_error')) + eval([[system("sh -c 'exit 5'")]]) + eq(5, eval('v:shell_error')) + eval([[system('this-should-not-exist')]]) + eq(127, eval('v:shell_error')) + end + end) + + describe('executes shell function', function() + local screen + + before_each(function() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + 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() + local comspecshell = eval("fnamemodify($COMSPEC, ':t')") + 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 + end) + + it('works with powershell', function() + helpers.set_shell_powershell() + eq('a\nb\n', eval([[system('echo a b')]])) + eq('C:\\\n', eval([[system('cd c:\; (Get-Location).Path')]])) + eq('a b\n', eval([[system('echo "a b"')]])) + end) + end + + it('`echo` and waits for its return', function() + feed(':call system("echo")<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :call system("echo") | + ]]) + end) + + it('prints verbose information', function() + screen:try_resize(72, 14) + feed(':4verbose echo system("echo hi")<cr>') + if iswin() then + screen:expect{any=[[Executing command: "'cmd.exe' '/s' '/c' '"echo hi"'"]]} + else + screen:expect{any=[[Executing command: "'/[^']*sh' '%-c' 'echo hi'"]]} + end + feed('<cr>') + end) + + it('self and total time recorded separately', function() + local tempfile = helpers.tmpname() + + feed(':function! AlmostNoSelfTime()<cr>') + feed('echo system("echo hi")<cr>') + feed('endfunction<cr>') + + feed(':profile start ' .. tempfile .. '<cr>') + feed(':profile func AlmostNoSelfTime<cr>') + feed(':call AlmostNoSelfTime()<cr>') + feed(':profile dump<cr>') + + feed(':edit ' .. tempfile .. '<cr>') + + local command_total_time = tonumber(helpers.funcs.split(helpers.funcs.getline(7))[2]) + local command_self_time = tonumber(helpers.funcs.split(helpers.funcs.getline(7))[3]) + + helpers.neq(nil, command_total_time) + helpers.neq(nil, command_self_time) + end) + + 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>') + screen:expect([[ + | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | +]] .. (iswin() + and [[ + :call system("for /L %I in (1,0,2) do @echo y") |]] + or [[ + :call system("yes") |]])) + feed('<c-c>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + Type :qa! and press <E...all changes and exit Nvim | + ]]) + end) + end) + + describe('passing no input', function() + it('returns the program output', function() + if iswin() then + eq("echoed\n", eval('system("echo echoed")')) + else + eq("echoed", eval('system("echo -n echoed")')) + end + end) + it('to backgrounded command does not crash', function() + -- This is indeterminate, just exercise the codepath. May get E5677. + 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) + end + eq(2, eval("1+1")) -- Still alive? + end) + end) + + describe('passing input', function() + it('returns the program output', function() + eq("input", eval('system("cat -", "input")')) + end) + it('to backgrounded command does not crash', function() + -- This is indeterminate, just exercise the codepath. May get E5677. + 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) + end + eq(2, eval("1+1")) -- Still alive? + end) + it('works with an empty string', function() + eq("test\n", eval('system("echo test", "")')) + eq(2, eval("1+1")) -- Still alive? + end) + end) + + describe('passing a lot of input', function() + it('returns the program output', function() + local input = {} + -- write more than 1mb of data, which should be enough to overcome + -- the os buffer limit and force multiple event loop iterations to write + -- everything + for _ = 1, 0xffff do + input[#input + 1] = '01234567890ABCDEFabcdef' + end + input = table.concat(input, '\n') + nvim('set_var', 'input', input) + eq(input, eval('system("cat -", g:input)')) + end) + end) + + describe('Number input', function() + it('is treated as a buffer id', function() + command("put ='text in buffer 1'") + eq('\ntext in buffer 1\n', eval('system("cat", 1)')) + eq('Vim(echo):E86: Buffer 42 does not exist', + exc_exec('echo system("cat", 42)')) + end) + end) + + describe('with output containing NULs', function() + local fname = 'Xtest' + + before_each(create_file_with_nuls(fname)) + after_each(delete_file(fname)) + + it('replaces NULs by SOH characters', function() + eq('part1\001part2\001part3\n', eval([[system('"cat" "]]..fname..[["')]])) + end) + end) + + describe('input passed as List', function() + it('joins List items with linefeed characters', function() + eq('line1\nline2\nline3', + eval("system('cat -', ['line1', 'line2', 'line3'])")) + end) + + -- Notice that NULs are converted to SOH when the data is read back. This + -- is inconsistent and is a good reason for the existence of the + -- `systemlist()` function, where input and output map to the same + -- characters(see the following tests with `systemlist()` below) + describe('with linefeed characters inside List items', function() + it('converts linefeed characters to NULs', function() + eq('l1\001p2\nline2\001a\001b\nl3', + eval([[system('cat -', ["l1\np2", "line2\na\nb", 'l3'])]])) + end) + end) + + describe('with leading/trailing whitespace characters on items', function() + it('preserves whitespace, replacing linefeeds by NULs', function() + eq('line \nline2\001\n\001line3', + eval([[system('cat -', ['line ', "line2\n", "\nline3"])]])) + end) + end) + end) + + describe("with a program that doesn't close stdout", function() + if not xclip then + pending('missing `xclip`', function() end) + else + it('will exit properly after passing input', function() + eq('', eval([[system('xclip -i -loops 1 -selection clipboard', 'clip-data')]])) + eq('clip-data', eval([[system('xclip -o -selection clipboard')]])) + end) + end + end) +end) + +describe('systemlist()', function() + -- Similar to `system()`, but returns List instead of String. + before_each(clear) + + it('sets v:shell_error', function() + if iswin() then + eval([[systemlist("cmd.exe /c exit")]]) + eq(0, eval('v:shell_error')) + eval([[systemlist("cmd.exe /c exit 1")]]) + eq(1, eval('v:shell_error')) + eval([[systemlist("cmd.exe /c exit 5")]]) + eq(5, eval('v:shell_error')) + eval([[systemlist('this-should-not-exist')]]) + eq(1, eval('v:shell_error')) + else + eval([[systemlist("sh -c 'exit'")]]) + eq(0, eval('v:shell_error')) + eval([[systemlist("sh -c 'exit 1'")]]) + eq(1, eval('v:shell_error')) + eval([[systemlist("sh -c 'exit 5'")]]) + eq(5, eval('v:shell_error')) + eval([[systemlist('this-should-not-exist')]]) + eq(127, eval('v:shell_error')) + end + end) + + describe('executes shell function', function() + local screen + + before_each(function() + screen = Screen.new() + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + it('`echo` and waits for its return', function() + feed(':call systemlist("echo")<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :call systemlist("echo") | + ]]) + end) + + it('`yes` interrupted with CTRL-C', function() + feed(':call systemlist("yes | xargs")<cr>') + screen:expect([[ + | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + :call systemlist("yes | xargs") | + ]]) + feed('<c-c>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + ~ | + Type :qa! and press <E...all changes and exit Nvim | + ]]) + end) + end) + + describe('passing string with linefeed characters as input', function() + it('splits the output on linefeed characters', function() + eq({'abc', 'def', 'ghi'}, eval([[systemlist("cat -", "abc\ndef\nghi")]])) + end) + end) + + describe('passing a lot of input', function() + it('returns the program output', function() + local input = {} + for _ = 1, 0xffff do + input[#input + 1] = '01234567890ABCDEFabcdef' + end + nvim('set_var', 'input', input) + eq(input, eval('systemlist("cat -", g:input)')) + end) + end) + + describe('with output containing NULs', function() + local fname = 'Xtest' + + before_each(function() + command('set ff=unix') + create_file_with_nuls(fname)() + end) + after_each(delete_file(fname)) + + it('replaces NULs by newline characters', function() + eq({'part1\npart2\npart3'}, eval([[systemlist('"cat" "]]..fname..[["')]])) + end) + end) + + describe('input passed as List', function() + it('joins list items with linefeed characters', function() + eq({'line1', 'line2', 'line3'}, + eval("systemlist('cat -', ['line1', 'line2', 'line3'])")) + end) + + -- Unlike `system()` which uses SOH to represent NULs, with `systemlist()` + -- input and ouput are the same. + describe('with linefeed characters inside list items', function() + it('converts linefeed characters to NULs', function() + eq({'l1\np2', 'line2\na\nb', 'l3'}, + eval([[systemlist('cat -', ["l1\np2", "line2\na\nb", 'l3'])]])) + end) + end) + + describe('with leading/trailing whitespace characters on items', function() + it('preserves whitespace, replacing linefeeds by NULs', function() + eq({'line ', 'line2\n', '\nline3'}, + eval([[systemlist('cat -', ['line ', "line2\n", "\nline3"])]])) + end) + end) + end) + + describe('handles empty lines', function() + it('in the middle', function() + eq({'line one','','line two'}, eval("systemlist('cat',['line one','','line two'])")) + end) + + it('in the beginning', function() + eq({'','line one','line two'}, eval("systemlist('cat',['','line one','line two'])")) + end) + end) + + describe('when keepempty option is', function() + it('0, ignores trailing newline', function() + eq({'aa','bb'}, eval("systemlist('cat',['aa','bb'],0)")) + eq({'aa','bb'}, eval("systemlist('cat',['aa','bb',''],0)")) + end) + + it('1, preserves trailing newline', function() + eq({'aa','bb'}, eval("systemlist('cat',['aa','bb'],1)")) + eq({'aa','bb',''}, eval("systemlist('cat',['aa','bb',''],2)")) + end) + end) + + describe("with a program that doesn't close stdout", function() + if not xclip then + pending('missing `xclip`', function() end) + else + it('will exit properly after passing input', function() + eq({}, eval( + "systemlist('xclip -i -loops 1 -selection clipboard', ['clip', 'data'])")) + eq({'clip', 'data'}, eval( + "systemlist('xclip -o -selection clipboard')")) + end) + end + end) +end) diff --git a/test/functional/eval/timer_spec.lua b/test/functional/eval/timer_spec.lua index 2f83edb9e4..124b9625c3 100644 --- a/test/functional/eval/timer_spec.lua +++ b/test/functional/eval/timer_spec.lua @@ -1,8 +1,9 @@ local helpers = require('test.functional.helpers')(after_each) local Screen = require('test.functional.ui.screen') -local ok, feed, eq, eval = helpers.ok, helpers.feed, helpers.eq, helpers.eval +local feed, eq, eval = helpers.feed, helpers.eq, helpers.eval local source, nvim_async, run = helpers.source, helpers.nvim_async, helpers.run -local clear, execute, funcs = helpers.clear, helpers.execute, helpers.funcs +local clear, command, funcs = helpers.clear, helpers.command, helpers.funcs +local curbufmeths = helpers.curbufmeths describe('timers', function() before_each(function() @@ -16,14 +17,14 @@ describe('timers', function() end) it('works one-shot', function() - execute("call timer_start(50, 'MyHandler')") + command("call timer_start(50, 'MyHandler')") eq(0,eval("g:val")) run(nil, nil, nil, 200) eq(1,eval("g:val")) end) it('works one-shot when repeat=0', function() - execute("call timer_start(50, 'MyHandler', {'repeat': 0})") + command("call timer_start(50, 'MyHandler', {'repeat': 0})") eq(0,eval("g:val")) run(nil, nil, nil, 200) eq(1,eval("g:val")) @@ -31,14 +32,14 @@ describe('timers', function() it('works with repeat two', function() - execute("call timer_start(50, 'MyHandler', {'repeat': 2})") + command("call timer_start(50, 'MyHandler', {'repeat': 2})") eq(0,eval("g:val")) run(nil, nil, nil, 300) eq(2,eval("g:val")) end) it('are triggered during sleep', function() - execute("call timer_start(50, 'MyHandler', {'repeat': 2})") + command("call timer_start(50, 'MyHandler', {'repeat': 2})") nvim_async("command", "sleep 10") eq(0,eval("g:val")) run(nil, nil, nil, 300) @@ -48,7 +49,7 @@ describe('timers', function() it('works with zero timeout', function() -- timer_start does still not invoke the callback immediately eq(0,eval("[timer_start(0, 'MyHandler', {'repeat': 1000}), g:val][1]")) - run(nil, nil, nil, 300) + run(nil, nil, nil, 400) eq(1000,eval("g:val")) end) @@ -62,19 +63,77 @@ describe('timers', function() end) it('are paused when event processing is disabled', function() - -- this is not the intended behavior, but at least there will - -- not be a burst of queued up callbacks - execute("call timer_start(50, 'MyHandler', {'repeat': 2})") + command("call timer_start(50, 'MyHandler', {'repeat': -1})") run(nil, nil, nil, 100) local count = eval("g:val") + -- shows two line error message and thus invokes the return prompt. + -- if we start to allow event processing here, we need to change this test. + feed(':throw "fatal error"<CR>') + run(nil, nil, nil, 300) + feed("<cr>") + local diff = eval("g:val") - count + assert(0 <= diff and diff <= 4, + 'expected (0 <= diff <= 4), got: '..tostring(diff)) + end) + + it('are triggered in blocking getchar() call', function() + command("call timer_start(50, 'MyHandler', {'repeat': -1})") nvim_async("command", "let g:c = getchar()") run(nil, nil, nil, 300) feed("c") - local diff = eval("g:val") - count - ok(0 <= diff and diff <= 2) + local count = eval("g:val") + assert(count >= 3, 'expected count >= 3, got: '..tostring(count)) eq(99, eval("g:c")) end) + it('can invoke redraw in blocking getchar() call', function() + local screen = Screen.new(40, 6) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold=true, foreground=Screen.colors.Blue}, + }) + + curbufmeths.set_lines(0, -1, true, {"ITEM 1", "ITEM 2"}) + source([[ + func! AddItem(timer) + call nvim_buf_set_lines(0, 2, 2, v:true, ['ITEM 3']) + call getchar(1) + redraw + endfunc + call timer_start(200, 'AddItem') + ]]) + nvim_async("command", "let g:c2 = getchar()") + + screen:expect([[ + ITEM 1 | + ITEM 2 | + {1:~ }| + {1:~ }| + {1:~ }| + ^ | + ]]) + + screen:expect([[ + ITEM 1 | + ITEM 2 | + ITEM 3 | + {1:~ }| + {1:~ }| + ^ | + ]]) + + feed("3") + eq(51, eval("g:c2")) + screen:expect([[ + ^ITEM 1 | + ITEM 2 | + ITEM 3 | + {1:~ }| + {1:~ }| + | + ]]) + end) + it('can be stopped', function() local t = eval("timer_start(50, 'MyHandler', {'repeat': -1})") eq(0,eval("g:val")) @@ -83,9 +142,10 @@ describe('timers', function() local count = eval("g:val") run(nil, nil, nil, 300) local count2 = eval("g:val") - ok(4 <= count and count <= 7) -- when count is eval:ed after timer_stop this should be non-racy eq(count, count2) + assert(3 <= count and count <= 7, + 'expected (3 <= count <= 7), got: '..tostring(count)) end) it('can be stopped from the handler', function() @@ -99,7 +159,7 @@ describe('timers', function() endif endfunc ]]) - execute("call timer_start(50, 'MyHandler', {'repeat': -1})") + command("call timer_start(50, 'MyHandler', {'repeat': -1})") eq(0,eval("g:val")) run(nil, nil, nil, 300) eq(3,eval("g:val")) @@ -112,33 +172,60 @@ describe('timers', function() let g:val2 += 1 endfunc ]]) - execute("call timer_start(50, 'MyHandler', {'repeat': 3})") - execute("call timer_start(100, 'MyHandler2', {'repeat': 2})") + command("call timer_start(20, 'MyHandler', {'repeat': 3})") + command("call timer_start(40, 'MyHandler2', {'repeat': 2})") run(nil, nil, nil, 300) eq(3,eval("g:val")) eq(2,eval("g:val2")) end) + it('do not crash when processing events in the handler', function() + source([[ + let g:val = 0 + func! MyHandler(timer) + call timer_stop(a:timer) + sleep 100m + let g:val += 1 + endfunc + ]]) + command("call timer_start(5, 'MyHandler', {'repeat': 1})") + run(nil, nil, nil, 300) + eq(1,eval("g:val")) + end) + + it("doesn't mess up the cmdline", function() local screen = Screen.new(40, 6) screen:attach() - screen:set_default_attr_ignore({{bold=true, foreground=Screen.colors.Blue}}) + screen:set_default_attr_ids( {[0] = {bold=true, foreground=255}} ) source([[ + let g:val = 0 func! MyHandler(timer) echo "evil" + let g:val = 1 endfunc ]]) - execute("call timer_start(100, 'MyHandler', {'repeat': 1})") + command("call timer_start(100, 'MyHandler', {'repeat': 1})") feed(":good") - screen:sleep(200) screen:expect([[ | - ~ | - ~ | - ~ | - ~ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| :good^ | ]]) + + screen:expect{grid=[[ + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + :good^ | + ]], intermediate=true, timeout=200} + + eq(1, eval('g:val')) 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/eval/writefile_spec.lua b/test/functional/eval/writefile_spec.lua new file mode 100644 index 0000000000..0bb7523d7e --- /dev/null +++ b/test/functional/eval/writefile_spec.lua @@ -0,0 +1,146 @@ +local helpers = require('test.functional.helpers')(after_each) +local lfs = require('lfs') + +local clear = helpers.clear +local eq = helpers.eq +local funcs = helpers.funcs +local meths = helpers.meths +local exc_exec = helpers.exc_exec +local read_file = helpers.read_file +local write_file = helpers.write_file +local redir_exec = helpers.redir_exec + +local fname = 'Xtest-functional-eval-writefile' +local dname = fname .. '.d' +local dfname_tail = '1' +local dfname = dname .. '/' .. dfname_tail +local ddname_tail = '2' +local ddname = dname .. '/' .. ddname_tail + +before_each(function() + lfs.mkdir(dname) + lfs.mkdir(ddname) + clear() +end) + +after_each(function() + os.remove(fname) + os.remove(dfname) + lfs.rmdir(ddname) + lfs.rmdir(dname) +end) + +describe('writefile()', function() + it('writes empty list to a file', function() + eq(nil, read_file(fname)) + eq(0, funcs.writefile({}, fname)) + eq('', read_file(fname)) + os.remove(fname) + eq(nil, read_file(fname)) + eq(0, funcs.writefile({}, fname, 'b')) + eq('', read_file(fname)) + os.remove(fname) + eq(nil, read_file(fname)) + eq(0, funcs.writefile({}, fname, 'ab')) + eq('', read_file(fname)) + os.remove(fname) + eq(nil, read_file(fname)) + eq(0, funcs.writefile({}, fname, 'a')) + eq('', read_file(fname)) + end) + + it('writes list with an empty string to a file', function() + eq(0, exc_exec( + ('call writefile([$XXX_NONEXISTENT_VAR_XXX], "%s", "b")'):format( + fname))) + eq('', read_file(fname)) + eq(0, exc_exec(('call writefile([$XXX_NONEXISTENT_VAR_XXX], "%s")'):format( + fname))) + eq('\n', read_file(fname)) + end) + + it('appends to a file', function() + eq(nil, read_file(fname)) + eq(0, funcs.writefile({'abc', 'def', 'ghi'}, fname)) + eq('abc\ndef\nghi\n', read_file(fname)) + eq(0, funcs.writefile({'jkl'}, fname, 'a')) + eq('abc\ndef\nghi\njkl\n', read_file(fname)) + os.remove(fname) + eq(nil, read_file(fname)) + eq(0, funcs.writefile({'abc', 'def', 'ghi'}, fname, 'b')) + eq('abc\ndef\nghi', read_file(fname)) + eq(0, funcs.writefile({'jkl'}, fname, 'ab')) + eq('abc\ndef\nghijkl', read_file(fname)) + end) + + it('correctly treats NLs', function() + eq(0, funcs.writefile({'\na\nb\n'}, fname, 'b')) + eq('\0a\0b\0', read_file(fname)) + eq(0, funcs.writefile({'a\n\n\nb'}, fname, 'b')) + eq('a\0\0\0b', read_file(fname)) + end) + + it('writes with s and S', function() + eq(0, funcs.writefile({'\na\nb\n'}, fname, 'bs')) + eq('\0a\0b\0', read_file(fname)) + eq(0, funcs.writefile({'a\n\n\nb'}, fname, 'bS')) + eq('a\0\0\0b', read_file(fname)) + end) + + it('correctly overwrites file', function() + eq(0, funcs.writefile({'\na\nb\n'}, fname, 'b')) + eq('\0a\0b\0', read_file(fname)) + eq(0, funcs.writefile({'a\n'}, fname, 'b')) + eq('a\0', read_file(fname)) + end) + + it('shows correct file name when supplied numbers', function() + meths.set_current_dir(dname) + eq('\nE482: Can\'t open file 2 for writing: illegal operation on a directory', + redir_exec(('call writefile([42], %s)'):format(ddname_tail))) + end) + + it('errors out with invalid arguments', function() + write_file(fname, 'TEST') + eq('\nE119: Not enough arguments for function: writefile', + redir_exec('call writefile()')) + eq('\nE119: Not enough arguments for function: writefile', + redir_exec('call writefile([])')) + eq('\nE118: Too many arguments for function: writefile', + redir_exec(('call writefile([], "%s", "b", 1)'):format(fname))) + for _, arg in ipairs({'0', '0.0', 'function("tr")', '{}', '"test"'}) do + eq('\nE686: Argument of writefile() must be a List', + redir_exec(('call writefile(%s, "%s", "b")'):format(arg, fname))) + end + for _, args in ipairs({'[], %s, "b"', '[], "' .. fname .. '", %s'}) do + eq('\nE806: using Float as a String', + redir_exec(('call writefile(%s)'):format(args:format('0.0')))) + eq('\nE730: using List as a String', + redir_exec(('call writefile(%s)'):format(args:format('[]')))) + eq('\nE731: using Dictionary as a String', + redir_exec(('call writefile(%s)'):format(args:format('{}')))) + eq('\nE729: using Funcref as a String', + redir_exec(('call writefile(%s)'):format(args:format('function("tr")')))) + end + eq('\nE5060: Unknown flag: «»', + redir_exec(('call writefile([], "%s", "bs«»")'):format(fname))) + eq('TEST', read_file(fname)) + end) + + it('does not write to file if error in list', function() + local args = '["tset"] + repeat([%s], 3), "' .. fname .. '"' + eq('\nE805: Expected a Number or a String, Float found', + redir_exec(('call writefile(%s)'):format(args:format('0.0')))) + eq(nil, read_file(fname)) + write_file(fname, 'TEST') + eq('\nE745: Expected a Number or a String, List found', + redir_exec(('call writefile(%s)'):format(args:format('[]')))) + eq('TEST', read_file(fname)) + eq('\nE728: Expected a Number or a String, Dictionary found', + redir_exec(('call writefile(%s)'):format(args:format('{}')))) + eq('TEST', read_file(fname)) + eq('\nE703: Expected a Number or a String, Funcref found', + redir_exec(('call writefile(%s)'):format(args:format('function("tr")')))) + eq('TEST', read_file(fname)) + end) +end) |