From d8de4eb685e35646c7d541e9a75bdc296127b7e2 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Fri, 17 Sep 2021 09:16:40 -0700 Subject: test: reorg #15698 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Subdirectories like "visual", "insert", "normal" encourage people to separate *related* tests for no good reason. Typically the _mode_ is not the relevant topic of a test (and when it is, _then_ create an appropriate describe() or it()). Solution: - Delete the various `test/functional//` subdirectories, move their tests to more meaningful topics. - Rename `…/normal/` to `…/editor/`. - Move or merge `…/visual/*` and `…/insert/*` tests into here where appropriate. - Rename `…/eval/` to `…/vimscript/`. - Move `…/viml/*` into here also. * test(reorg): insert/* => editor/mode_insert_spec.lua * test(reorg): cmdline/* => editor/mode_cmdline_spec.lua * test(reorg): eval core tests => eval_spec.lua --- test/functional/vimscript/api_functions_spec.lua | 167 +++++ test/functional/vimscript/buf_functions_spec.lua | 306 ++++++++ test/functional/vimscript/changedtick_spec.lua | 142 ++++ .../vimscript/container_functions_spec.lua | 24 + test/functional/vimscript/ctx_functions_spec.lua | 406 +++++++++++ test/functional/vimscript/environ_spec.lua | 80 +++ test/functional/vimscript/errorlist_spec.lua | 84 +++ test/functional/vimscript/eval_spec.lua | 146 ++++ test/functional/vimscript/executable_spec.lua | 218 ++++++ test/functional/vimscript/execute_spec.lua | 337 +++++++++ test/functional/vimscript/exepath_spec.lua | 40 ++ test/functional/vimscript/fnamemodify_spec.lua | 156 ++++ test/functional/vimscript/functions_spec.lua | 20 + test/functional/vimscript/getline_spec.lua | 39 + test/functional/vimscript/glob_spec.lua | 28 + test/functional/vimscript/has_spec.lua | 66 ++ test/functional/vimscript/hostname_spec.lua | 20 + test/functional/vimscript/input_spec.lua | 483 +++++++++++++ test/functional/vimscript/json_functions_spec.lua | 795 +++++++++++++++++++++ test/functional/vimscript/lang_spec.lua | 30 + test/functional/vimscript/let_spec.lua | 93 +++ test/functional/vimscript/map_functions_spec.lua | 163 +++++ test/functional/vimscript/match_functions_spec.lua | 157 ++++ .../functional/vimscript/minmax_functions_spec.lua | 51 ++ test/functional/vimscript/modeline_spec.lua | 19 + .../vimscript/msgpack_functions_spec.lua | 755 +++++++++++++++++++ test/functional/vimscript/null_spec.lua | 170 +++++ test/functional/vimscript/operators_spec.lua | 28 + test/functional/vimscript/printf_spec.lua | 92 +++ test/functional/vimscript/reltime_spec.lua | 53 ++ test/functional/vimscript/server_spec.lua | 156 ++++ test/functional/vimscript/setpos_spec.lua | 64 ++ test/functional/vimscript/sort_spec.lua | 57 ++ test/functional/vimscript/special_vars_spec.lua | 190 +++++ test/functional/vimscript/string_spec.lua | 277 +++++++ test/functional/vimscript/system_spec.lua | 589 +++++++++++++++ test/functional/vimscript/timer_spec.lua | 265 +++++++ test/functional/vimscript/uniq_spec.lua | 31 + test/functional/vimscript/vvar_event_spec.lua | 15 + test/functional/vimscript/wait_spec.lua | 78 ++ test/functional/vimscript/writefile_spec.lua | 156 ++++ 41 files changed, 7046 insertions(+) create mode 100644 test/functional/vimscript/api_functions_spec.lua create mode 100644 test/functional/vimscript/buf_functions_spec.lua create mode 100644 test/functional/vimscript/changedtick_spec.lua create mode 100644 test/functional/vimscript/container_functions_spec.lua create mode 100644 test/functional/vimscript/ctx_functions_spec.lua create mode 100644 test/functional/vimscript/environ_spec.lua create mode 100644 test/functional/vimscript/errorlist_spec.lua create mode 100644 test/functional/vimscript/eval_spec.lua create mode 100644 test/functional/vimscript/executable_spec.lua create mode 100644 test/functional/vimscript/execute_spec.lua create mode 100644 test/functional/vimscript/exepath_spec.lua create mode 100644 test/functional/vimscript/fnamemodify_spec.lua create mode 100644 test/functional/vimscript/functions_spec.lua create mode 100644 test/functional/vimscript/getline_spec.lua create mode 100644 test/functional/vimscript/glob_spec.lua create mode 100644 test/functional/vimscript/has_spec.lua create mode 100644 test/functional/vimscript/hostname_spec.lua create mode 100644 test/functional/vimscript/input_spec.lua create mode 100644 test/functional/vimscript/json_functions_spec.lua create mode 100644 test/functional/vimscript/lang_spec.lua create mode 100644 test/functional/vimscript/let_spec.lua create mode 100644 test/functional/vimscript/map_functions_spec.lua create mode 100644 test/functional/vimscript/match_functions_spec.lua create mode 100644 test/functional/vimscript/minmax_functions_spec.lua create mode 100644 test/functional/vimscript/modeline_spec.lua create mode 100644 test/functional/vimscript/msgpack_functions_spec.lua create mode 100644 test/functional/vimscript/null_spec.lua create mode 100644 test/functional/vimscript/operators_spec.lua create mode 100644 test/functional/vimscript/printf_spec.lua create mode 100644 test/functional/vimscript/reltime_spec.lua create mode 100644 test/functional/vimscript/server_spec.lua create mode 100644 test/functional/vimscript/setpos_spec.lua create mode 100644 test/functional/vimscript/sort_spec.lua create mode 100644 test/functional/vimscript/special_vars_spec.lua create mode 100644 test/functional/vimscript/string_spec.lua create mode 100644 test/functional/vimscript/system_spec.lua create mode 100644 test/functional/vimscript/timer_spec.lua create mode 100644 test/functional/vimscript/uniq_spec.lua create mode 100644 test/functional/vimscript/vvar_event_spec.lua create mode 100644 test/functional/vimscript/wait_spec.lua create mode 100644 test/functional/vimscript/writefile_spec.lua (limited to 'test/functional/vimscript') diff --git a/test/functional/vimscript/api_functions_spec.lua b/test/functional/vimscript/api_functions_spec.lua new file mode 100644 index 0000000000..d07e74d40e --- /dev/null +++ b/test/functional/vimscript/api_functions_spec.lua @@ -0,0 +1,167 @@ +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, pcall_err = helpers.insert, helpers.pcall_err +local meths = helpers.meths + +describe('eval-API', 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')") + 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):E5555: API call: Wrong type for argument 4 when calling nvim_buf_set_lines, expecting Boolean', err) + + err = exc_exec('call nvim_buf_set_lines(0, 0, -1, v:true, "string")') + eq('Vim(call):E5555: API call: Wrong type for argument 5 when calling nvim_buf_set_lines, expecting ArrayOf(String)', err) + + err = exc_exec('call nvim_buf_get_number("0")') + eq('Vim(call):E5555: API call: Wrong type for argument 1 when calling nvim_buf_get_number, expecting Buffer', err) + + err = exc_exec('call nvim_buf_line_count(17)') + eq('Vim(call):E5555: API call: Invalid buffer id: 17', err) + end) + + it('cannot change texts if textlocked', function() + command("autocmd TextYankPost ++once call nvim_buf_set_lines(0, 0, -1, v:false, [])") + eq('Vim(call):E5555: API call: E523: Not allowed here', pcall_err(command, "normal! yy")) + 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:~ }| + | + ]]) + end) + + it('cannot be called from sandbox', function() + eq('Vim(call):E48: Not allowed in sandbox', + pcall_err(command, "sandbox call nvim_input('ievil')")) + eq({''}, meths.buf_get_lines(0, 0, -1, true)) + end) + + it('converts blobs to API strings', function() + command('let g:v1 = nvim__id(0z68656c6c6f)') + command('let g:v2 = nvim__id(v:_null_blob)') + eq(1, eval('type(g:v1)')) + eq(1, eval('type(g:v2)')) + eq('hello', eval('g:v1')) + eq('', eval('g:v2')) + end) +end) diff --git a/test/functional/vimscript/buf_functions_spec.lua b/test/functional/vimscript/buf_functions_spec.lua new file mode 100644 index 0000000000..e957e5f5af --- /dev/null +++ b/test/functional/vimscript/buf_functions_spec.lua @@ -0,0 +1,306 @@ +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 pcall_err = helpers.pcall_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'}) do + eq('Vim(call):E5299: Expected a Number or a String, Boolean found', + exc_exec('call ' .. func:format(var))) + end + eq('Vim(call):E5300: Expected a Number or a String', + exc_exec('call ' .. func:format('v:null'))) + 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(1, funcs.getbufvar(1, '&hidden')) + eq(1, funcs.getbufvar(1, '&l:hidden')) + eq(1, 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 + command("setl number") + -- (note that current window’s buffer is 2, but getbufvar() receives 1) + eq({id=2}, 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(true, meths.get_option('hidden')) + funcs.setbufvar(1, '&hidden', 0) + eq(false, 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')) + eq('Vim:E46: Cannot change read-only variable "b:changedtick"', + pcall_err(funcs.setbufvar, 1, 'changedtick', true)) + eq(2, funcs.getbufvar(1, 'changedtick')) + end) +end) diff --git a/test/functional/vimscript/changedtick_spec.lua b/test/functional/vimscript/changedtick_spec.lua new file mode 100644 index 0000000000..99406d9d7a --- /dev/null +++ b/test/functional/vimscript/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 pcall_err = helpers.pcall_err +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('Key is read-only: changedtick', + pcall_err(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('Key is read-only: changedtick', + pcall_err(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:let cmdline=""') + 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/vimscript/container_functions_spec.lua b/test/functional/vimscript/container_functions_spec.lua new file mode 100644 index 0000000000..04a3248c49 --- /dev/null +++ b/test/functional/vimscript/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/vimscript/ctx_functions_spec.lua b/test/functional/vimscript/ctx_functions_spec.lua new file mode 100644 index 0000000000..f23adbc556 --- /dev/null +++ b/test/functional/vimscript/ctx_functions_spec.lua @@ -0,0 +1,406 @@ +local helpers = require('test.functional.helpers')(after_each) + +local call = helpers.call +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local eval = helpers.eval +local feed = helpers.feed +local map = helpers.tbl_map +local nvim = helpers.nvim +local parse_context = helpers.parse_context +local redir_exec = helpers.redir_exec +local source = helpers.source +local trim = helpers.trim +local write_file = helpers.write_file +local pcall_err = helpers.pcall_err + +describe('context functions', function() + local fname1 = 'Xtest-functional-eval-ctx1' + local fname2 = 'Xtest-functional-eval-ctx2' + local outofbounds = + 'Vim:E475: Invalid value for argument index: out of bounds' + + before_each(function() + clear() + write_file(fname1, "1\n2\n3") + write_file(fname2, "a\nb\nc") + end) + + after_each(function() + os.remove(fname1) + os.remove(fname2) + end) + + describe('ctxpush/ctxpop', function() + it('saves and restores registers properly', function() + local regs = {'1', '2', '3', 'a'} + local vals = {'1', '2', '3', 'hjkl'} + feed('i123ddddddqahjklq') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + call('ctxpush') + call('ctxpush', {'regs'}) + + map(function(r) call('setreg', r, {}) end, regs) + eq({'', '', '', ''}, + map(function(r) return trim(call('getreg', r)) end, regs)) + + call('ctxpop') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + + map(function(r) call('setreg', r, {}) end, regs) + eq({'', '', '', ''}, + map(function(r) return trim(call('getreg', r)) end, regs)) + + call('ctxpop') + eq(vals, map(function(r) return trim(call('getreg', r)) end, regs)) + end) + + it('saves and restores jumplist properly', function() + command('edit '..fname1) + feed('G') + feed('gg') + command('edit '..fname2) + local jumplist = call('getjumplist') + call('ctxpush') + call('ctxpush', {'jumps'}) + + command('clearjumps') + eq({{}, 0}, call('getjumplist')) + + call('ctxpop') + eq(jumplist, call('getjumplist')) + + command('clearjumps') + eq({{}, 0}, call('getjumplist')) + + call('ctxpop') + eq(jumplist, call('getjumplist')) + end) + + it('saves and restores buffer list properly', function() + command('edit '..fname1) + command('edit '..fname2) + command('edit TEST') + local bufs = call('map', call('getbufinfo'), 'v:val.name') + call('ctxpush') + call('ctxpush', {'bufs'}) + + command('%bwipeout') + eq({''}, call('map', call('getbufinfo'), 'v:val.name')) + + call('ctxpop') + eq({'', unpack(bufs)}, call('map', call('getbufinfo'), 'v:val.name')) + + command('%bwipeout') + eq({''}, call('map', call('getbufinfo'), 'v:val.name')) + + call('ctxpop') + eq({'', unpack(bufs)}, call('map', call('getbufinfo'), 'v:val.name')) + end) + + it('saves and restores global variables properly', function() + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpush') + call('ctxpush', {'gvars'}) + + nvim('del_var', 'one') + nvim('del_var', 'Two') + nvim('del_var', 'THREE') + eq('Vim:E121: Undefined variable: g:one', pcall_err(eval, 'g:one')) + eq('Vim:E121: Undefined variable: g:Two', pcall_err(eval, 'g:Two')) + eq('Vim:E121: Undefined variable: g:THREE', pcall_err(eval, 'g:THREE')) + + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + + nvim('del_var', 'one') + nvim('del_var', 'Two') + nvim('del_var', 'THREE') + eq('Vim:E121: Undefined variable: g:one', pcall_err(eval, 'g:one')) + eq('Vim:E121: Undefined variable: g:Two', pcall_err(eval, 'g:Two')) + eq('Vim:E121: Undefined variable: g:THREE', pcall_err(eval, 'g:THREE')) + + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + end) + + it('saves and restores script functions properly', function() + source([[ + function s:greet(name) + echom 'Hello, '.a:name.'!' + endfunction + + function s:greet_all(name, ...) + echom 'Hello, '.a:name.'!' + for more in a:000 + echom 'Hello, '.more.'!' + endfor + endfunction + + function Greet(name) + call call('s:greet', [a:name]) + endfunction + + function GreetAll(name, ...) + call call('s:greet_all', extend([a:name], a:000)) + endfunction + + function SaveSFuncs() + call ctxpush(['sfuncs']) + endfunction + + function DeleteSFuncs() + delfunction s:greet + delfunction s:greet_all + endfunction + + function RestoreFuncs() + call ctxpop() + endfunction + ]]) + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('SaveSFuncs') + call('DeleteSFuncs') + + eq('\nError detected while processing function Greet:'.. + '\nline 1:'.. + '\nE117: Unknown function: s:greet', + redir_exec([[call Greet('World')]])) + eq('\nError detected while processing function GreetAll:'.. + '\nline 1:'.. + '\nE117: Unknown function: s:greet_all', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('RestoreFuncs') + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + end) + + it('saves and restores functions properly', function() + source([[ + function Greet(name) + echom 'Hello, '.a:name.'!' + endfunction + + function GreetAll(name, ...) + echom 'Hello, '.a:name.'!' + for more in a:000 + echom 'Hello, '.more.'!' + endfor + endfunction + ]]) + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + + call('ctxpush', {'funcs'}) + command('delfunction Greet') + command('delfunction GreetAll') + + eq('Vim:E117: Unknown function: Greet', pcall_err(call, 'Greet', 'World')) + eq('Vim:E117: Unknown function: GreetAll', + pcall_err(call, 'GreetAll', 'World', 'One', 'Two', 'Three')) + + call('ctxpop') + + eq('\nHello, World!', redir_exec([[call Greet('World')]])) + eq('\nHello, World!'.. + '\nHello, One!'.. + '\nHello, Two!'.. + '\nHello, Three!', + redir_exec([[call GreetAll('World', 'One', 'Two', 'Three')]])) + end) + + it('errors out when context stack is empty', function() + local err = 'Vim:Context stack is empty' + eq(err, pcall_err(call, 'ctxpop')) + eq(err, pcall_err(call, 'ctxpop')) + call('ctxpush') + call('ctxpush') + call('ctxpop') + call('ctxpop') + eq(err, pcall_err(call, 'ctxpop')) + end) + end) + + describe('ctxsize()', function() + it('returns context stack size', function() + eq(0, call('ctxsize')) + call('ctxpush') + eq(1, call('ctxsize')) + call('ctxpush') + eq(2, call('ctxsize')) + call('ctxpush') + eq(3, call('ctxsize')) + call('ctxpop') + eq(2, call('ctxsize')) + call('ctxpop') + eq(1, call('ctxsize')) + call('ctxpop') + eq(0, call('ctxsize')) + end) + end) + + describe('ctxget()', function() + it('errors out when index is out of bounds', function() + eq(outofbounds, pcall_err(call, 'ctxget')) + call('ctxpush') + eq(outofbounds, pcall_err(call, 'ctxget', 1)) + call('ctxpop') + eq(outofbounds, pcall_err(call, 'ctxget', 0)) + end) + + it('returns context dictionary at index in context stack', function() + feed('i123ddddddqahjklq') + command('edit! '..fname1) + feed('G') + feed('gg') + command('edit '..fname2) + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + + local with_regs = { + ['regs'] = { + {['rt'] = 1, ['rc'] = {'1'}, ['n'] = 49, ['ru'] = true}, + {['rt'] = 1, ['rc'] = {'2'}, ['n'] = 50}, + {['rt'] = 1, ['rc'] = {'3'}, ['n'] = 51}, + {['rc'] = {'hjkl'}, ['n'] = 97}, + } + } + + local with_jumps = { + ['jumps'] = eval(([[ + filter(map(getjumplist()[0], 'filter( + { "f": expand("#".v:val.bufnr.":p"), "l": v:val.lnum }, + { k, v -> k != "l" || v != 1 })'), '!empty(v:val.f)') + ]]):gsub('\n', '')) + } + + local with_bufs = { + ['bufs'] = eval([[ + filter(map(getbufinfo(), '{ "f": v:val.name }'), '!empty(v:val.f)') + ]]) + } + + local with_gvars = { + ['gvars'] = {{'one', 1}, {'Two', 2}, {'THREE', 3}} + } + + local with_all = { + ['regs'] = with_regs['regs'], + ['jumps'] = with_jumps['jumps'], + ['bufs'] = with_bufs['bufs'], + ['gvars'] = with_gvars['gvars'], + } + + call('ctxpush') + eq(with_all, parse_context(call('ctxget'))) + eq(with_all, parse_context(call('ctxget', 0))) + + call('ctxpush', {'gvars'}) + eq(with_gvars, parse_context(call('ctxget'))) + eq(with_gvars, parse_context(call('ctxget', 0))) + eq(with_all, parse_context(call('ctxget', 1))) + + call('ctxpush', {'bufs'}) + eq(with_bufs, parse_context(call('ctxget'))) + eq(with_bufs, parse_context(call('ctxget', 0))) + eq(with_gvars, parse_context(call('ctxget', 1))) + eq(with_all, parse_context(call('ctxget', 2))) + + call('ctxpush', {'jumps'}) + eq(with_jumps, parse_context(call('ctxget'))) + eq(with_jumps, parse_context(call('ctxget', 0))) + eq(with_bufs, parse_context(call('ctxget', 1))) + eq(with_gvars, parse_context(call('ctxget', 2))) + eq(with_all, parse_context(call('ctxget', 3))) + + call('ctxpush', {'regs'}) + eq(with_regs, parse_context(call('ctxget'))) + eq(with_regs, parse_context(call('ctxget', 0))) + eq(with_jumps, parse_context(call('ctxget', 1))) + eq(with_bufs, parse_context(call('ctxget', 2))) + eq(with_gvars, parse_context(call('ctxget', 3))) + eq(with_all, parse_context(call('ctxget', 4))) + + call('ctxpop') + eq(with_jumps, parse_context(call('ctxget'))) + eq(with_jumps, parse_context(call('ctxget', 0))) + eq(with_bufs, parse_context(call('ctxget', 1))) + eq(with_gvars, parse_context(call('ctxget', 2))) + eq(with_all, parse_context(call('ctxget', 3))) + + call('ctxpop') + eq(with_bufs, parse_context(call('ctxget'))) + eq(with_bufs, parse_context(call('ctxget', 0))) + eq(with_gvars, parse_context(call('ctxget', 1))) + eq(with_all, parse_context(call('ctxget', 2))) + + call('ctxpop') + eq(with_gvars, parse_context(call('ctxget'))) + eq(with_gvars, parse_context(call('ctxget', 0))) + eq(with_all, parse_context(call('ctxget', 1))) + + call('ctxpop') + eq(with_all, parse_context(call('ctxget'))) + eq(with_all, parse_context(call('ctxget', 0))) + end) + end) + + describe('ctxset()', function() + it('errors out when index is out of bounds', function() + eq(outofbounds, pcall_err(call, 'ctxset', {dummy = 1})) + call('ctxpush') + eq(outofbounds, pcall_err(call, 'ctxset', {dummy = 1}, 1)) + call('ctxpop') + eq(outofbounds, pcall_err(call, 'ctxset', {dummy = 1}, 0)) + end) + + it('sets context dictionary at index in context stack', function() + nvim('set_var', 'one', 1) + nvim('set_var', 'Two', 2) + nvim('set_var', 'THREE', 3) + call('ctxpush') + local ctx1 = call('ctxget') + nvim('set_var', 'one', 'a') + nvim('set_var', 'Two', 'b') + nvim('set_var', 'THREE', 'c') + call('ctxpush') + call('ctxpush') + local ctx2 = call('ctxget') + + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + call('ctxset', ctx1) + call('ctxset', ctx2, 2) + call('ctxpop') + eq({1, 2 ,3}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpop') + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + nvim('set_var', 'one', 1.5) + eq({1.5, 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + call('ctxpop') + eq({'a', 'b' ,'c'}, eval('[g:one, g:Two, g:THREE]')) + end) + end) +end) diff --git a/test/functional/vimscript/environ_spec.lua b/test/functional/vimscript/environ_spec.lua new file mode 100644 index 0000000000..9e19568249 --- /dev/null +++ b/test/functional/vimscript/environ_spec.lua @@ -0,0 +1,80 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eq = helpers.eq +local environ = helpers.funcs.environ +local exists = helpers.funcs.exists +local system = helpers.funcs.system +local nvim_prog = helpers.nvim_prog +local command = helpers.command +local eval = helpers.eval +local setenv = helpers.funcs.setenv + +describe('environment variables', function() + it('environ() handles empty env variable', function() + clear({env={EMPTY_VAR=""}}) + eq("", environ()['EMPTY_VAR']) + eq(nil, environ()['DOES_NOT_EXIST']) + end) + + it('exists() handles empty env variable', function() + clear({env={EMPTY_VAR=""}}) + eq(1, exists('$EMPTY_VAR')) + eq(0, exists('$DOES_NOT_EXIST')) + end) +end) + +describe('empty $HOME', function() + local original_home = os.getenv('HOME') + + -- recover $HOME after each test + after_each(function() + if original_home ~= nil then + setenv('HOME', original_home) + end + os.remove('test_empty_home') + os.remove('./~') + end) + + local function tilde_in_cwd() + -- get files in cwd + command("let test_empty_home_cwd_files = split(globpath('.', '*'), '\n')") + -- get the index of the file named '~' + command('let test_empty_home_tilde_index = index(test_empty_home_cwd_files, "./~")') + return eval('test_empty_home_tilde_index') ~= -1 + end + + local function write_and_test_tilde() + system({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--headless', + '-c', 'write test_empty_home', '+q'}) + eq(false, tilde_in_cwd()) + end + + it("'~' folder not created in cwd if $HOME and related env not defined", function() + command("unlet $HOME") + write_and_test_tilde() + + command("let $HOMEDRIVE='C:'") + command("let $USERPROFILE='C:\\'") + write_and_test_tilde() + + command("unlet $HOMEDRIVE") + write_and_test_tilde() + + command("unlet $USERPROFILE") + write_and_test_tilde() + + command("let $HOME='%USERPROFILE%'") + command("let $USERPROFILE='C:\\'") + write_and_test_tilde() + end) + + it("'~' folder not created in cwd if writing a file with invalid $HOME", function() + setenv('HOME', '/path/does/not/exist') + write_and_test_tilde() + end) + + it("'~' folder not created in cwd if writing a file with $HOME=''", function() + command("let $HOME=''") + write_and_test_tilde() + end) +end) diff --git a/test/functional/vimscript/errorlist_spec.lua b/test/functional/vimscript/errorlist_spec.lua new file mode 100644 index 0000000000..077d816903 --- /dev/null +++ b/test/functional/vimscript/errorlist_spec.lua @@ -0,0 +1,84 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local exc_exec = helpers.exc_exec +local get_cur_win_var = helpers.curwinmeths.get_var + +describe('setqflist()', function() + local setqflist = helpers.funcs.setqflist + + before_each(clear) + + it('requires a list for {list}', function() + eq('Vim(call):E714: List required', exc_exec('call setqflist("foo")')) + eq('Vim(call):E714: List required', exc_exec('call setqflist(5)')) + eq('Vim(call):E714: List required', exc_exec('call setqflist({})')) + end) + + it('requires a string for {action}', function() + eq('Vim(call):E928: String required', exc_exec('call setqflist([], 5)')) + eq('Vim(call):E928: String required', exc_exec('call setqflist([], [])')) + eq('Vim(call):E928: String required', exc_exec('call setqflist([], {})')) + end) + + it('sets w:quickfix_title', function() + setqflist({''}, 'r', 'foo') + command('copen') + eq('foo', get_cur_win_var('quickfix_title')) + setqflist({}, 'r', {['title'] = 'qf_title'}) + eq('qf_title', get_cur_win_var('quickfix_title')) + end) + + it('allows string {what} for backwards compatibility', function() + setqflist({}, 'r', '5') + command('copen') + eq('5', get_cur_win_var('quickfix_title')) + end) + + it('requires a dict for {what}', function() + eq('Vim(call):E715: Dictionary required', exc_exec('call setqflist([], "r", function("function"))')) + end) +end) + +describe('setloclist()', function() + local setloclist = helpers.funcs.setloclist + + before_each(clear) + + it('requires a list for {list}', function() + eq('Vim(call):E714: List required', exc_exec('call setloclist(0, "foo")')) + eq('Vim(call):E714: List required', exc_exec('call setloclist(0, 5)')) + eq('Vim(call):E714: List required', exc_exec('call setloclist(0, {})')) + end) + + it('requires a string for {action}', function() + eq('Vim(call):E928: String required', exc_exec('call setloclist(0, [], 5)')) + eq('Vim(call):E928: String required', exc_exec('call setloclist(0, [], [])')) + eq('Vim(call):E928: String required', exc_exec('call setloclist(0, [], {})')) + end) + + it('sets w:quickfix_title for the correct window', function() + command('rightbelow vsplit') + setloclist(1, {}, 'r', 'foo') + setloclist(2, {}, 'r', 'bar') + command('lopen') + eq('bar', get_cur_win_var('quickfix_title')) + command('lclose | wincmd w | lopen') + eq('foo', get_cur_win_var('quickfix_title')) + end) + + it("doesn't crash when when window is closed in the middle #13721", function() + helpers.insert([[ + hello world]]) + + command("vsplit") + command("autocmd WinLeave * :call nvim_win_close(0, v:true)") + + command("call setloclist(0, [])") + command("lopen") + + helpers.assert_alive() + end) +end) diff --git a/test/functional/vimscript/eval_spec.lua b/test/functional/vimscript/eval_spec.lua new file mode 100644 index 0000000000..e1459ab5b8 --- /dev/null +++ b/test/functional/vimscript/eval_spec.lua @@ -0,0 +1,146 @@ +-- Tests for core Vimscript "eval" behavior. +-- +-- See also: +-- let_spec.lua +-- null_spec.lua +-- operators_spec.lua +-- +-- Tests for the Vimscript |functions| library should live in: +-- test/functional/vimscript/_spec.lua +-- test/functional/vimscript/functions_spec.lua + +local helpers = require('test.functional.helpers')(after_each) + +local lfs = require('lfs') +local clear = helpers.clear +local eq = helpers.eq +local exc_exec = helpers.exc_exec +local eval = helpers.eval +local command = helpers.command +local write_file = helpers.write_file +local meths = helpers.meths +local sleep = helpers.sleep +local poke_eventloop = helpers.poke_eventloop +local feed = helpers.feed + +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) + +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) + +describe('List support code', function() + local dur + local min_dur = 8 + local len = 131072 + + 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():let t_bl = copy(bl)') + sleep(min_dur / 16 * 1000) + feed('') + poke_eventloop() + 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():let t_j = join(bl)') + sleep(min_dur / 16 * 1000) + feed('') + poke_eventloop() + 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/vimscript/executable_spec.lua b/test/functional/vimscript/executable_spec.lua new file mode 100644 index 0000000000..28aefb72e5 --- /dev/null +++ b/test/functional/vimscript/executable_spec.lua @@ -0,0 +1,218 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq, clear, call, iswin, write_file, command = + helpers.eq, helpers.clear, helpers.call, helpers.iswin, helpers.write_file, + helpers.command +local exc_exec = helpers.exc_exec +local eval = helpers.eval + +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)) + command('let $PATH = fnamemodify("./test/functional/fixtures/bin", ":p")') + eq(1, call('executable', 'null')) + eq(1, call('executable', 'true')) + eq(1, call('executable', 'false')) + end) + + it('fails for invalid values', function() + for _, input in ipairs({'""', 'v:null', 'v:true', 'v:false', '{}', '[]'}) do + eq('Vim(call):E928: String required', exc_exec('call executable('..input..')')) + end + command('let $PATH = fnamemodify("./test/functional/fixtures/bin", ":p")') + for _, input in ipairs({'v:null', 'v:true', 'v:false'}) do + eq('Vim(call):E928: String required', exc_exec('call executable('..input..')')) + end + 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 + eq('arg1=lemon;arg2=sky;arg3=tree;', + call('system', sibling_exe..' lemon sky tree')) + end + eq(expected, call('executable', sibling_exe)) + 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() + eq(0, call('executable', 'Xtest_not_executable')) + eq(0, call('executable', './Xtest_not_executable')) + end) + + it('set, unqualified and not in $PATH', function() + eq(0, call('executable', 'Xtest_executable')) + end) + + it('set, qualified as a path', function() + local expected = iswin() and 0 or 1 + eq(expected, 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('system([…]), jobstart([…]) use $PATHEXT #9569', function() + -- Invoking `cmdscript` should find/execute `cmdscript.cmd`. + eq('much success\n', call('system', {'test/functional/fixtures/cmdscript'})) + assert(0 < call('jobstart', {'test/functional/fixtures/cmdscript'})) + end) + + it('full path with extension', function() + -- Some executable we can expect in the test env. + local exe = 'printargs-test' + local exedir = 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 = 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("with weird $PATHEXT", function() + clear({env={PATHEXT=';'}}) + eq(0, call('executable', '.\\test_executable_zzz')) + clear({env={PATHEXT=';;;.zzz;;'}}) + eq(1, call('executable', '.\\test_executable_zzz')) + end) + + it("unqualified filename, Unix-style 'shell'", function() + clear({env={PATHEXT=''}}) + command('set shell=sh') + 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("relative path, Unix-style 'shell' (backslashes)", function() + clear({env={PATHEXT=''}}) + command('set shell=bash.exe') + 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) + + it('unqualified filename, $PATHEXT contains dot', function() + clear({env={PATHEXT='.;.zzz'}}) + for _,ext in ipairs(exts) do + eq(1, call('executable', 'test_executable_'..ext..'.'..ext)) + end + eq(1, call('executable', 'test_executable_zzz.zzz')) + clear({env={PATHEXT='.zzz;.'}}) + 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('relative path, $PATHEXT contains dot (backslashes)', function() + clear({env={PATHEXT='.;.zzz'}}) + 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) + + it('ignores case of extension', function() + clear({env={PATHEXT='.ZZZ'}}) + eq(1, call('executable', 'test_executable_zzz.zzz')) + end) + + it('relative path does not search $PATH', function() + clear({env={PATHEXT=''}}) + eq(0, call('executable', './System32/notepad.exe')) + eq(0, call('executable', '.\\System32\\notepad.exe')) + eq(0, call('executable', '../notepad.exe')) + eq(0, call('executable', '..\\notepad.exe')) + end) +end) diff --git a/test/functional/vimscript/execute_spec.lua b/test/functional/vimscript/execute_spec.lua new file mode 100644 index 0000000000..fccf52935b --- /dev/null +++ b/test/functional/vimscript/execute_spec.lua @@ -0,0 +1,337 @@ +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)