diff options
Diffstat (limited to 'test')
44 files changed, 5220 insertions, 424 deletions
diff --git a/test/README.md b/test/README.md index df66f24626..2857cc0ecf 100644 --- a/test/README.md +++ b/test/README.md @@ -27,8 +27,8 @@ groups by the semantic component they are testing. Test behaviour is affected by environment variables. Currently supported (Functional, Unit, Benchmarks) (when Defined; when set to _1_; when defined, -treated as Integer; when defined, treated as String; !must be defined to -function properly): +treated as Integer; when defined, treated as String; when defined, treated as +Number; !must be defined to function properly): `GDB` (F) (D): makes nvim instances to be run under `gdbserver`. It will be accessible on `localhost:7777`: use `gdb build/bin/nvim`, type `target remote @@ -99,3 +99,12 @@ get backtrace from). approximately 90% of the tests. Should be used when finding cores is too hard for some reason. Normally (on OS X or when `NVIM_TEST_CORE_GLOB_DIRECTORY` is defined and this variable is not) cores are checked for after each test. + +`NVIM_TEST_RUN_TESTTEST` (U) (1): allows running `test/unit/testtest_spec.lua` +used to check how testing infrastructure works. + +`NVIM_TEST_TRACE_LEVEL` (U) (N): specifies unit tests tracing level: `0` +disables tracing (the fastest, but you get no data if tests crash and there was +no core dump generated), `1` or empty/undefined leaves only C function cals and +returns in the trace (faster then recording everything), `2` records all +function calls, returns and lua source lines exuecuted. diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 3348368a36..8f9f155110 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -153,6 +153,28 @@ describe('api', function() nvim('set_option', 'equalalways', false) ok(not nvim('get_option', 'equalalways')) end) + + it('works to get global value of local options', function() + eq(false, nvim('get_option', 'lisp')) + eq(8, nvim('get_option', 'shiftwidth')) + end) + + it('works to set global value of local options', function() + nvim('set_option', 'lisp', true) + eq(true, nvim('get_option', 'lisp')) + eq(false, helpers.curbuf('get_option', 'lisp')) + eq(nil, nvim('command_output', 'setglobal lisp?'):match('nolisp')) + eq('nolisp', nvim('command_output', 'setlocal lisp?'):match('nolisp')) + nvim('set_option', 'shiftwidth', 20) + eq('20', nvim('command_output', 'setglobal shiftwidth?'):match('%d+')) + eq('8', nvim('command_output', 'setlocal shiftwidth?'):match('%d+')) + end) + + it('most window-local options have no global value', function() + local status, err = pcall(nvim, 'get_option', 'foldcolumn') + eq(false, status) + ok(err:match('Invalid option name') ~= nil) + end) end) describe('nvim_{get,set}_current_buf, nvim_list_bufs', function() diff --git a/test/functional/autocmd/termclose_spec.lua b/test/functional/autocmd/termclose_spec.lua index ecda1bffb7..d4beab22e4 100644 --- a/test/functional/autocmd/termclose_spec.lua +++ b/test/functional/autocmd/termclose_spec.lua @@ -14,17 +14,11 @@ describe('TermClose event', function() nvim('set_option', 'shellcmdflag', 'EXE') end) - local function eq_err(expected, actual) - if expected ~= actual then - error('expected: '..tostring(expected)..', actual: '..tostring(actual)) - end - end - it('triggers when terminal job ends', function() command('autocmd TermClose * let g:test_termclose = 23') command('terminal') command('call jobstop(b:terminal_job_id)') - retry(nil, nil, function() eq_err(23, eval('g:test_termclose')) end) + retry(nil, nil, function() eq(23, eval('g:test_termclose')) end) end) it('reports the correct <abuf>', function() @@ -35,12 +29,12 @@ describe('TermClose event', function() eq(2, eval('bufnr("%")')) command('terminal') - retry(nil, nil, function() eq_err(3, eval('bufnr("%")')) end) + retry(nil, nil, function() eq(3, eval('bufnr("%")')) end) command('buffer 1') - retry(nil, nil, function() eq_err(1, eval('bufnr("%")')) end) + retry(nil, nil, function() eq(1, eval('bufnr("%")')) end) command('3bdelete!') - retry(nil, nil, function() eq_err('3', eval('g:abuf')) end) + retry(nil, nil, function() eq('3', eval('g:abuf')) end) end) end) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index e442c2a317..9ee91f2fe9 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -8,6 +8,7 @@ local clear, eq, eval, exc_exec, execute, feed, insert, neq, next_msg, nvim, local command = helpers.command local wait = helpers.wait local iswin = helpers.iswin +local get_pathsep = helpers.get_pathsep local Screen = require('test.functional.ui.screen') describe('jobs', function() @@ -65,7 +66,7 @@ describe('jobs', function() end) it('changes to given `cwd` directory', function() - local dir = eval('resolve(tempname())') + local dir = eval("resolve(tempname())"):gsub("/", get_pathsep()) mkdir(dir) nvim('command', "let g:job_opts.cwd = '" .. dir .. "'") if iswin() then diff --git a/test/functional/eval/buf_functions_spec.lua b/test/functional/eval/buf_functions_spec.lua new file mode 100644 index 0000000000..db50874c53 --- /dev/null +++ b/test/functional/eval/buf_functions_spec.lua @@ -0,0 +1,302 @@ +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 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() + lfs.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() + lfs.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')) + funcs.setbufvar(1, 'changedtick', true) + -- eq(true, bufmeths.get_var(buf1, 'changedtick')) + eq(2, funcs.getbufvar(1, '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/input_spec.lua b/test/functional/eval/input_spec.lua new file mode 100644 index 0000000000..393fc10175 --- /dev/null +++ b/test/functional/eval/input_spec.lua @@ -0,0 +1,38 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local feed = helpers.feed +local clear = helpers.clear +local command = helpers.command + +local screen + +before_each(function() + clear() + screen = Screen.new(25, 5) + screen:attach() +end) + +describe('input()', function() + it('works correctly with multiline prompts', function() + feed([[:call input("Test\nFoo")<CR>]]) + screen:expect([[ + {1:~ }| + {1:~ }| + {1:~ }| + Test | + Foo^ | + ]], {{bold=true, foreground=Screen.colors.Blue}}) + end) + it('works correctly with multiline prompts and :echohl', function() + command('hi Test ctermfg=Red guifg=Red term=bold') + feed([[:echohl Test | call input("Test\nFoo")<CR>]]) + screen:expect([[ + {1:~ }| + {1:~ }| + {1:~ }| + {2:Test} | + {2:Foo}^ | + ]], {{bold=true, foreground=Screen.colors.Blue}, {foreground=Screen.colors.Red}}) + 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..3150d89f62 --- /dev/null +++ b/test/functional/eval/match_functions_spec.lua @@ -0,0 +1,61 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local clear = helpers.clear +local funcs = helpers.funcs +local command = helpers.command + +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() + eq(-1, funcs.setmatches({{group=1, pattern=2, id=3, priority=4}})) + eq({}, funcs.getmatches()) + eq(-1, funcs.setmatches({{group=1, pos1={2}, pos2={6}, id=3, priority=4, conceal=5}})) + 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) 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/null_spec.lua b/test/functional/eval/null_spec.lua new file mode 100644 index 0000000000..6fd30caec9 --- /dev/null +++ b/test/functional/eval/null_spec.lua @@ -0,0 +1,138 @@ +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 map() should not return 0 without error + null_expr_test('does not crash map()', 'map(L, "v:val")', 0, 0) + -- FIXME map() should not return 0 without error + null_expr_test('does not crash filter()', 'filter(L, "1")', 0, 0) + -- FIXME map() should at least return L + null_expr_test('makes map() return v:_null_list', 'map(L, "v:val") is# L', 0, 0) + -- FIXME filter() should at least return L + null_expr_test('makes filter() return v:_null_list', 'map(L, "1") is# L', 0, 0) + -- FIXME add() should not return 1 at all + null_expr_test('does not crash add()', 'add(L, 0)', 0, 1) + 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}) + -- FIXME should be accepted by inputlist() + null_expr_test('is accepted as an empty list by inputlist()', + '[feedkeys("\\n"), inputlist(L)]', 'E686: Argument of inputlist() must be a List', {0, 0}) + -- FIXME should be accepted by writefile(), return {0, {}} + null_expr_test('is accepted as an empty list by writefile()', + ('[writefile(L, "%s"), readfile("%s")]'):format(tmpfname, tmpfname), + 'E484: Can\'t open file ' .. tmpfname, {0, {}}) + -- FIXME should give error message + null_expr_test('does not crash remove()', 'remove(L, 0)', 0, 0) + -- FIXME should return 0 + null_expr_test('is accepted by setqflist()', 'setqflist(L)', 0, -1) + -- FIXME should return 0 + null_expr_test('is accepted by setloclist()', 'setloclist(1, L)', 0, -1) + -- FIXME should return 0 + null_expr_test('is accepted by setmatches()', 'setmatches(L)', 0, -1) + -- FIXME should return empty list or error out + null_expr_test('is accepted by sort()', 'sort(L)', 0, 0) + -- FIXME Should return 1 + null_expr_test('is accepted by sort()', 'sort(L) is L', 0, 0) + -- FIXME should not error out + null_test('is accepted by :cexpr', 'cexpr L', 'Vim(cexpr):E777: String or List expected') + -- FIXME should not error out + null_test('is accepted by :lexpr', 'lexpr L', 'Vim(lexpr):E777: String or List expected') + null_test('is accepted by :for', 'for x in L|throw x|endfor', 0) + + -- 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) + -- FIXME Should return 1 + null_expr_test('is not locked', 'islocked("v:_null_list")', 0, 0) + + -- Crashes + + -- null_expr_test('does not crash setreg', 'setreg("x", L)', 0, 0) + -- null_expr_test('does not crash setline', 'setline(1, L)', 0, 0) + -- null_expr_test('does not crash system()', 'system("cat", L)', 0, '') + -- null_expr_test('does not crash systemlist()', 'systemlist("cat", L)', 0, {}) + + -- Correct behaviour + null_expr_test('does not crash append()', 'append(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('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) + 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}) + end) +end) diff --git a/test/functional/eval/sort_spec.lua b/test/functional/eval/sort_spec.lua new file mode 100644 index 0000000000..4e5a0afba4 --- /dev/null +++ b/test/functional/eval/sort_spec.lua @@ -0,0 +1,41 @@ +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 + +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) +end) diff --git a/test/functional/eval/string_spec.lua b/test/functional/eval/string_spec.lua index f6279e85e8..adc1af9b8e 100644 --- a/test/functional/eval/string_spec.lua +++ b/test/functional/eval/string_spec.lua @@ -7,7 +7,6 @@ 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 @@ -105,10 +104,8 @@ 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 @@ -120,11 +117,6 @@ describe('string() function', function() 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() diff --git a/test/functional/eval/timer_spec.lua b/test/functional/eval/timer_spec.lua index 4353619ff0..b3c4cd07eb 100644 --- a/test/functional/eval/timer_spec.lua +++ b/test/functional/eval/timer_spec.lua @@ -49,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) diff --git a/test/functional/eval/writefile_spec.lua b/test/functional/eval/writefile_spec.lua index 3052c616e0..2f84114b9b 100644 --- a/test/functional/eval/writefile_spec.lua +++ b/test/functional/eval/writefile_spec.lua @@ -80,6 +80,13 @@ describe('writefile()', function() 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)) @@ -115,6 +122,8 @@ describe('writefile()', function() 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) diff --git a/test/functional/ex_cmds/dict_notifications_spec.lua b/test/functional/ex_cmds/dict_notifications_spec.lua index 30753c34ac..e3b4a1c504 100644 --- a/test/functional/ex_cmds/dict_notifications_spec.lua +++ b/test/functional/ex_cmds/dict_notifications_spec.lua @@ -9,7 +9,7 @@ local eval = helpers.eval describe('dictionary change notifications', function() local channel - setup(function() + before_each(function() clear() channel = nvim('get_api_info')[1] nvim('set_var', 'channel', channel) @@ -18,19 +18,15 @@ describe('dictionary change notifications', function() -- the same set of tests are applied to top-level dictionaries(g:, b:, w: and -- t:) and a dictionary variable, so we generate them in the following -- function. - local function gentests(dict_expr, dict_expr_suffix, dict_init) - if not dict_expr_suffix then - dict_expr_suffix = '' - end - + local function gentests(dict_expr, dict_init) local function update(opval, key) if not key then key = 'watched' end if opval == '' then - nvim('command', "unlet "..dict_expr..dict_expr_suffix..key) + command(('unlet %s[\'%s\']'):format(dict_expr, key)) else - nvim('command', "let "..dict_expr..dict_expr_suffix..key.." "..opval) + command(('let %s[\'%s\'] %s'):format(dict_expr, key, opval)) end end @@ -48,9 +44,9 @@ describe('dictionary change notifications', function() eq({'notification', 'values', {key, vals}}, next_msg()) end - describe('watcher', function() + describe(dict_expr .. ' watcher', function() if dict_init then - setup(function() + before_each(function() source(dict_init) end) end @@ -58,7 +54,7 @@ describe('dictionary change notifications', function() before_each(function() source([[ function! g:Changed(dict, key, value) - if a:dict != ]]..dict_expr..[[ | + if a:dict isnot ]]..dict_expr..[[ | throw 'invalid dict' endif call rpcnotify(g:channel, 'values', a:key, a:value) @@ -143,6 +139,32 @@ describe('dictionary change notifications', function() ]]) end) + it('is triggered for empty keys', function() + command([[ + call dictwatcheradd(]]..dict_expr..[[, "", "g:Changed") + ]]) + update('= 1', '') + verify_value({new = 1}, '') + update('= 2', '') + verify_value({old = 1, new = 2}, '') + command([[ + call dictwatcherdel(]]..dict_expr..[[, "", "g:Changed") + ]]) + end) + + it('is triggered for empty keys when using catch-all *', function() + command([[ + call dictwatcheradd(]]..dict_expr..[[, "*", "g:Changed") + ]]) + update('= 1', '') + verify_value({new = 1}, '') + update('= 2', '') + verify_value({old = 1, new = 2}, '') + command([[ + call dictwatcherdel(]]..dict_expr..[[, "*", "g:Changed") + ]]) + end) + -- test a sequence of updates of different types to ensure proper memory -- management(with ASAN) local function test_updates(tests) @@ -190,10 +212,10 @@ describe('dictionary change notifications', function() gentests('b:') gentests('w:') gentests('t:') - gentests('g:dict_var', '.', 'let g:dict_var = {}') + gentests('g:dict_var', 'let g:dict_var = {}') describe('multiple watchers on the same dict/key', function() - setup(function() + before_each(function() source([[ function! g:Watcher1(dict, key, value) call rpcnotify(g:channel, '1', a:key, a:value) @@ -213,13 +235,37 @@ describe('dictionary change notifications', function() end) it('only removes watchers that fully match dict, key and callback', function() + nvim('command', 'let g:key = "value"') + eq({'notification', '1', {'key', {new = 'value'}}}, next_msg()) + eq({'notification', '2', {'key', {new = 'value'}}}, next_msg()) nvim('command', 'call dictwatcherdel(g:, "key", "g:Watcher1")') nvim('command', 'let g:key = "v2"') eq({'notification', '2', {'key', {old = 'value', new = 'v2'}}}, next_msg()) end) end) + it('errors out when adding to v:_null_dict', function() + command([[ + function! g:Watcher1(dict, key, value) + call rpcnotify(g:channel, '1', a:key, a:value) + endfunction + ]]) + eq('Vim(call):E46: Cannot change read-only variable "dictwatcheradd() argument"', + exc_exec('call dictwatcheradd(v:_null_dict, "x", "g:Watcher1")')) + end) + describe('errors', function() + before_each(function() + source([[ + function! g:Watcher1(dict, key, value) + call rpcnotify(g:channel, '1', a:key, a:value) + endfunction + function! g:Watcher2(dict, key, value) + call rpcnotify(g:channel, '2', a:key, a:value) + endfunction + ]]) + end) + -- WARNING: This suite depends on the above tests it('fails to remove if no watcher with matching callback is found', function() eq("Vim(call):Couldn't find a watcher matching key and callback", @@ -236,15 +282,24 @@ describe('dictionary change notifications', function() command('call dictwatcherdel(g:, "key", "g:InvalidCb")') end) - it('fails with empty keys', function() - eq("Vim(call):E713: Cannot use empty key for Dictionary", - exc_exec('call dictwatcheradd(g:, "", "g:Watcher1")')) - eq("Vim(call):E713: Cannot use empty key for Dictionary", - exc_exec('call dictwatcherdel(g:, "", "g:Watcher1")')) + it('fails to remove watcher from v:_null_dict', function() + eq("Vim(call):Couldn't find a watcher matching key and callback", + exc_exec('call dictwatcherdel(v:_null_dict, "x", "g:Watcher2")')) end) + --[[ + [ it("fails to add/remove if the callback doesn't exist", function() + [ eq("Vim(call):Function g:InvalidCb doesn't exist", + [ exc_exec('call dictwatcheradd(g:, "key", "g:InvalidCb")')) + [ eq("Vim(call):Function g:InvalidCb doesn't exist", + [ exc_exec('call dictwatcherdel(g:, "key", "g:InvalidCb")')) + [ end) + ]] + it('does not fail to replace a watcher function', function() source([[ + let g:key = 'v2' + call dictwatcheradd(g:, "key", "g:Watcher2") function! g:ReplaceWatcher2() function! g:Watcher2(dict, key, value) call rpcnotify(g:channel, '2b', a:key, a:value) diff --git a/test/functional/ex_cmds/quickfix_commands_spec.lua b/test/functional/ex_cmds/quickfix_commands_spec.lua new file mode 100644 index 0000000000..5ab34db3fb --- /dev/null +++ b/test/functional/ex_cmds/quickfix_commands_spec.lua @@ -0,0 +1,83 @@ +local helpers = require('test.functional.helpers')(after_each) + +local eq = helpers.eq +local clear = helpers.clear +local funcs = helpers.funcs +local command = helpers.command +local exc_exec = helpers.exc_exec +local write_file = helpers.write_file +local curbufmeths = helpers.curbufmeths + +local file_base = 'Xtest-functional-ex_cmds-quickfix_commands' + +before_each(clear) + +for _, c in ipairs({'l', 'c'}) do + local file = ('%s.%s'):format(file_base, c) + local filecmd = c .. 'file' + local getfcmd = c .. 'getfile' + local addfcmd = c .. 'addfile' + local getlist = (c == 'c') and funcs.getqflist or ( + function() return funcs.getloclist(0) end) + + describe((':%s*file commands'):format(c), function() + before_each(function() + write_file(file, ([[ + %s-1.res:700:10:Line 700 + %s-2.res:800:15:Line 800 + ]]):format(file, file)) + end) + after_each(function() + os.remove(file) + end) + + it('work', function() + command(('%s %s'):format(filecmd, file)) + -- Second line of each entry (i.e. `nr=-1, …`) was obtained from actual + -- results. First line (i.e. `{lnum=…`) was obtained from legacy test. + local list = { + {lnum=700, col=10, text='Line 700', + nr=-1, bufnr=2, valid=1, pattern='', vcol=0, ['type']=''}, + {lnum=800, col=15, text='Line 800', + nr=-1, bufnr=3, valid=1, pattern='', vcol=0, ['type']=''}, + } + eq(list, getlist()) + eq(('%s-1.res'):format(file), funcs.bufname(list[1].bufnr)) + eq(('%s-2.res'):format(file), funcs.bufname(list[2].bufnr)) + + -- Run cfile/lfile from a modified buffer + command('enew!') + curbufmeths.set_lines(1, 1, true, {'Quickfix'}) + eq(('Vim(%s):E37: No write since last change (add ! to override)'):format( + filecmd), + exc_exec(('%s %s'):format(filecmd, file))) + + write_file(file, ([[ + %s-3.res:900:30:Line 900 + ]]):format(file)) + command(('%s %s'):format(addfcmd, file)) + list[#list + 1] = { + lnum=900, col=30, text='Line 900', + nr=-1, bufnr=5, valid=1, pattern='', vcol=0, ['type']='', + } + eq(list, getlist()) + eq(('%s-3.res'):format(file), funcs.bufname(list[3].bufnr)) + + write_file(file, ([[ + %s-1.res:222:77:Line 222 + %s-2.res:333:88:Line 333 + ]]):format(file, file)) + command('enew!') + command(('%s %s'):format(getfcmd, file)) + list = { + {lnum=222, col=77, text='Line 222', + nr=-1, bufnr=2, valid=1, pattern='', vcol=0, ['type']=''}, + {lnum=333, col=88, text='Line 333', + nr=-1, bufnr=3, valid=1, pattern='', vcol=0, ['type']=''}, + } + eq(list, getlist()) + eq(('%s-1.res'):format(file), funcs.bufname(list[1].bufnr)) + eq(('%s-2.res'):format(file), funcs.bufname(list[2].bufnr)) + end) + end) +end diff --git a/test/functional/ex_cmds/write_spec.lua b/test/functional/ex_cmds/write_spec.lua index 4ac9f312ef..9c2687971e 100644 --- a/test/functional/ex_cmds/write_spec.lua +++ b/test/functional/ex_cmds/write_spec.lua @@ -1,15 +1,28 @@ local helpers = require('test.functional.helpers')(after_each) +local lfs = require('lfs') local eq, eval, clear, write_file, execute, source, insert = helpers.eq, helpers.eval, helpers.clear, helpers.write_file, helpers.execute, helpers.source, helpers.insert +local redir_exec = helpers.redir_exec +local exc_exec = helpers.exc_exec +local command = helpers.command +local funcs = helpers.funcs +local meths = helpers.meths if helpers.pending_win32(pending) then return end +local fname = 'Xtest-functional-ex_cmds-write' +local fname_bak = fname .. '~' +local fname_broken = fname_bak .. 'broken' + describe(':write', function() local function cleanup() os.remove('test_bkc_file.txt') os.remove('test_bkc_link.txt') os.remove('test_fifo') + os.remove(fname) + os.remove(fname_bak) + os.remove(fname_broken) end before_each(function() clear() @@ -63,4 +76,34 @@ describe(':write', function() eq(text.."\n", fifo:read("*all")) fifo:close() end) + + it('errors out correctly', function() + command('let $HOME=""') + eq(funcs.fnamemodify('.', ':p:h'), funcs.fnamemodify('.', ':p:h:~')) + -- Message from check_overwrite + eq(('\nE17: "'..funcs.fnamemodify('.', ':p:h')..'" is a directory'), + redir_exec('write .')) + meths.set_option('writeany', true) + -- Message from buf_write + eq(('\nE502: "." is a directory'), + redir_exec('write .')) + funcs.mkdir(fname_bak) + meths.set_option('backupdir', '.') + meths.set_option('backup', true) + write_file(fname, 'content0') + eq(0, exc_exec('edit ' .. fname)) + funcs.setline(1, 'TTY') + eq('Vim(write):E510: Can\'t make backup file (add ! to override)', + exc_exec('write')) + meths.set_option('backup', false) + funcs.setfperm(fname, 'r--------') + eq('Vim(write):E505: "Xtest-functional-ex_cmds-write" is read-only (add ! to override)', + exc_exec('write')) + os.remove(fname) + os.remove(fname_bak) + write_file(fname_bak, 'TTYX') + lfs.link(fname_bak .. ('/xxxxx'):rep(20), fname, true) + eq('Vim(write):E166: Can\'t open linked file for writing', + exc_exec('write!')) + end) end) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 13a0cff137..335cf3c3ff 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -16,6 +16,7 @@ local eq = global_helpers.eq local ok = global_helpers.ok local map = global_helpers.map local filter = global_helpers.filter +local dedent = global_helpers.dedent local start_dir = lfs.currentdir() -- XXX: NVIM_PROG takes precedence, QuickBuild sets it. @@ -23,7 +24,7 @@ local nvim_prog = os.getenv('NVIM_PROG') or os.getenv('NVIM_PRG') or 'build/bin/ -- Default settings for the test session. local nvim_set = 'set shortmess+=I background=light noswapfile noautoindent' ..' laststatus=1 undodir=. directory=. viewdir=. backupdir=.' - ..' belloff= noshowcmd noruler' + ..' belloff= noshowcmd noruler nomore' local nvim_argv = {nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--cmd', nvim_set, '--embed'} @@ -191,28 +192,6 @@ local function nvim_feed(input) end end -local function dedent(str) - -- find minimum common indent across lines - local indent = nil - for line in str:gmatch('[^\n]+') do - local line_indent = line:match('^%s+') or '' - if indent == nil or #line_indent < #indent then - indent = line_indent - end - end - if indent == nil or #indent == 0 then - -- no minimum common indent - return str - end - -- create a pattern for the indent - indent = indent:gsub('%s', '[ \t]') - -- strip it from the first line - str = str:gsub('^'..indent, '') - -- strip it from the remaining lines - str = str:gsub('[\n]'..indent, '\n') - return str -end - local function feed(...) for _, v in ipairs({...}) do nvim_feed(dedent(v)) @@ -570,6 +549,10 @@ local curbufmeths = create_callindex(curbuf) local curwinmeths = create_callindex(curwin) local curtabmeths = create_callindex(curtab) +local function get_pathsep() + return funcs.fnamemodify('.', ':p'):sub(-1) +end + local M = { prepend_argv = prepend_argv, clear = clear, @@ -635,6 +618,7 @@ local M = { tmpname = tmpname, meth_pcall = meth_pcall, NIL = mpack.NIL, + get_pathsep = get_pathsep, } return function(after_each) diff --git a/test/functional/legacy/051_highlight_spec.lua b/test/functional/legacy/051_highlight_spec.lua index ef392d8c67..d4d9b7d997 100644 --- a/test/functional/legacy/051_highlight_spec.lua +++ b/test/functional/legacy/051_highlight_spec.lua @@ -16,7 +16,7 @@ describe(':highlight', function() local screen = Screen.new(35, 10) screen:attach() -- Basic test if ":highlight" doesn't crash - execute('highlight') + execute('set more', 'highlight') -- FIXME(tarruda): We need to be sure the prompt is displayed before -- continuing, or risk a race condition where some of the following input -- is discarded resulting in test failure diff --git a/test/functional/legacy/062_tab_pages_spec.lua b/test/functional/legacy/062_tab_pages_spec.lua index d5b10b160e..71a0a77354 100644 --- a/test/functional/legacy/062_tab_pages_spec.lua +++ b/test/functional/legacy/062_tab_pages_spec.lua @@ -99,10 +99,6 @@ describe('tab pages', function() eq(7, eval('tabpagenr()')) execute('tabmove') eq(10, eval('tabpagenr()')) - execute('tabmove -20') - eq(1, eval('tabpagenr()')) - execute('tabmove +20') - eq(10, eval('tabpagenr()')) execute('0tabmove') eq(1, eval('tabpagenr()')) execute('$tabmove') @@ -172,7 +168,7 @@ describe('tab pages', function() C tabnext 1 autocmd TabDestructive TabEnter * nested \ :C tabnext 2 | C tabclose 3 - C tabnext 3 + C tabnext 2 let g:r+=[tabpagenr().'/'.tabpagenr('$')] endfunction call Test() @@ -233,22 +229,14 @@ describe('tab pages', function() WinEnter TabEnter BufEnter - === tabnext 3 === - BufLeave - WinLeave - TabLeave - WinEnter - TabEnter === tabnext 2 === - BufLeave WinLeave TabLeave WinEnter TabEnter === tabnext 2 === === tabclose 3 === - BufEnter - === tabclose 3 === 2/2]]) + eq(2, eval("tabpagenr('$')")) end) end) diff --git a/test/functional/legacy/063_match_and_matchadd_spec.lua b/test/functional/legacy/063_match_and_matchadd_spec.lua index 298e0a31ea..5818bb6b3a 100644 --- a/test/functional/legacy/063_match_and_matchadd_spec.lua +++ b/test/functional/legacy/063_match_and_matchadd_spec.lua @@ -97,11 +97,10 @@ describe('063: Test for ":match", "matchadd()" and related functions', function( -- Check that "setmatches()" will not add two matches with the same ID. The -- expected behaviour (for now) is to add the first match but not the - -- second and to return 0 (even though it is a matter of debate whether - -- this can be considered successful behaviour). + -- second and to return -1. execute("let r1 = setmatches([{'group': 'MyGroup1', 'pattern': 'TODO', 'priority': 10, 'id': 1}, {'group': 'MyGroup2', 'pattern': 'FIXME', 'priority': 10, 'id': 1}])") feed("<cr>") - eq(0, eval("r1")) + eq(-1, eval("r1")) eq({{group = 'MyGroup1', pattern = 'TODO', priority = 10, id = 1}}, eval('getmatches()')) -- Check that "setmatches()" returns 0 if successful and otherwise -1. diff --git a/test/functional/legacy/arglist_spec.lua b/test/functional/legacy/arglist_spec.lua index f5e3522972..b9075620dc 100644 --- a/test/functional/legacy/arglist_spec.lua +++ b/test/functional/legacy/arglist_spec.lua @@ -269,4 +269,33 @@ describe('argument list commands', function() eq(0, eval('argidx()')) execute('%argd') end) + + + it('test for autocommand that redefines the argument list, when doing ":all"', function() + execute('autocmd BufReadPost Xxx2 next Xxx2 Xxx1') + execute("call writefile(['test file Xxx1'], 'Xxx1')") + execute("call writefile(['test file Xxx2'], 'Xxx2')") + execute("call writefile(['test file Xxx3'], 'Xxx3')") + + execute('new') + -- redefine arglist; go to Xxx1 + execute('next! Xxx1 Xxx2 Xxx3') + -- open window for all args + execute('all') + eq('test file Xxx1', eval('getline(1)')) + execute('wincmd w') + execute('wincmd w') + eq('test file Xxx1', eval('getline(1)')) + -- should now be in Xxx2 + execute('rewind') + eq('test file Xxx2', eval('getline(1)')) + + execute('autocmd! BufReadPost Xxx2') + execute('enew! | only') + execute("call delete('Xxx1')") + execute("call delete('Xxx2')") + execute("call delete('Xxx3')") + execute('argdelete Xxx*') + execute('bwipe! Xxx1 Xxx2 Xxx3') + end) end) diff --git a/test/functional/normal/fold_spec.lua b/test/functional/normal/fold_spec.lua index a2a2a35a8b..fc055c4e7a 100644 --- a/test/functional/normal/fold_spec.lua +++ b/test/functional/normal/fold_spec.lua @@ -5,9 +5,16 @@ local insert = helpers.insert local feed = helpers.feed local expect = helpers.expect local execute = helpers.execute +local funcs = helpers.funcs +local foldlevel = funcs.foldlevel +local foldclosedend = funcs.foldclosedend +local eq = helpers.eq describe('Folds', function() + local tempfname = 'Xtest-fold.txt' clear() + before_each(function() execute('enew!') end) + after_each(function() os.remove(tempfname) end) it('manual folding adjusts with filter', function() insert([[ 1 @@ -44,4 +51,293 @@ describe('Folds', function() 8 9]]) end) + describe('adjusting folds after :move', function() + local function manually_fold_indent() + -- setting foldmethod twice is a trick to get vim to set the folds for me + execute('set foldmethod=indent', 'set foldmethod=manual') + -- Ensure that all folds will get closed (makes it easier to test the + -- length of folds). + execute('set foldminlines=0') + -- Start with all folds open (so :move ranges aren't affected by closed + -- folds). + execute('%foldopen!') + end + + local function get_folds() + local rettab = {} + for i = 1, funcs.line('$') do + table.insert(rettab, foldlevel(i)) + end + return rettab + end + + local function test_move_indent(insert_string, move_command) + -- This test is easy because we just need to ensure that the resulting + -- fold is the same as calculated when creating folds from scratch. + insert(insert_string) + execute(move_command) + local after_move_folds = get_folds() + -- Doesn't change anything, but does call foldUpdateAll() + execute('set foldminlines=0') + eq(after_move_folds, get_folds()) + -- Set up the buffer with insert_string for the manual fold testing. + execute('enew!') + insert(insert_string) + manually_fold_indent() + execute(move_command) + end + + it('neither closes nor corrupts folds', function() + test_move_indent([[ +a + a + a + a + a + a +a + a + a + a + a + a +a + a + a + a + a + a]], '7,12m0') + expect([[ +a + a + a + a + a + a +a + a + a + a + a + a +a + a + a + a + a + a]]) + -- lines are not closed, folds are correct + for i = 1,funcs.line('$') do + eq(-1, funcs.foldclosed(i)) + if i == 1 or i == 7 or i == 13 then + eq(0, foldlevel(i)) + elseif i == 4 then + eq(2, foldlevel(i)) + else + eq(1, foldlevel(i)) + end + end + -- folds are not corrupted + feed('zM') + eq(6, foldclosedend(2)) + eq(12, foldclosedend(8)) + eq(18, foldclosedend(14)) + end) + it("doesn't split a fold when the move is within it", function() + test_move_indent([[ +a + a + a + a + a + a + a + a + a +a]], '5m6') + eq({0, 1, 1, 2, 2, 2, 2, 1, 1, 0}, get_folds()) + end) + it('truncates folds that end in the moved range', function() + test_move_indent([[ +a + a + a + a + a +a +a]], '4,5m6') + eq({0, 1, 2, 0, 0, 0, 0}, get_folds()) + end) + it('moves folds that start between moved range and destination', function() + test_move_indent([[ +a + a + a + a + a +a +a + a + a + a +a +a + a]], '3,4m$') + eq({0, 1, 1, 0, 0, 1, 2, 1, 0, 0, 1, 0, 0}, get_folds()) + end) + it('does not affect folds outside changed lines', function() + test_move_indent([[ + a + a + a +a +a +a + a + a + a]], '4m5') + eq({1, 1, 1, 0, 0, 0, 1, 1, 1}, get_folds()) + end) + it('moves and truncates folds that start in moved range', function() + test_move_indent([[ +a + a + a + a + a +a +a +a +a +a]], '1,3m7') + eq({0, 0, 0, 0, 0, 1, 2, 0, 0, 0}, get_folds()) + end) + it('breaks a fold when moving text into it', function() + test_move_indent([[ +a + a + a + a + a +a +a]], '$m4') + eq({0, 1, 2, 2, 0, 0, 0}, get_folds()) + end) + it('adjusts correctly when moving a range backwards', function() + test_move_indent([[ +a + a + a + a +a]], '2,3m0') + eq({1, 2, 0, 0, 0}, get_folds()) + end) + end) + it('updates correctly on :read', function() + -- luacheck: ignore 621 + helpers.write_file(tempfname, [[ + a + + + a]]) + insert([[ + a + a + a + a + ]]) + execute('set foldmethod=indent', '2', '%foldopen') + execute('read ' .. tempfname) + -- Just to check we have the correct file text. + expect([[ + a + a + a + + + a + a + a + ]]) + for i = 1,2 do + eq(1, funcs.foldlevel(i)) + end + for i = 3,5 do + eq(0, funcs.foldlevel(i)) + end + for i = 6,8 do + eq(1, funcs.foldlevel(i)) + end + end) + it('combines folds when removing separating space', function() + -- luacheck: ignore 621 + insert([[ + a + a + a + a + a + a + a + a + ]]) + execute('set foldmethod=indent', '3,5d') + eq(5, funcs.foldclosedend(1)) + end) + it("doesn't combine folds that have a specified end", function() + insert([[ + {{{ + }}} + + + + {{{ + + }}} + ]]) + execute('set foldmethod=marker', '3,5d', '%foldclose') + eq(2, funcs.foldclosedend(1)) + end) + it('splits folds according to >N and <N with foldexpr', function() + helpers.source([[ + function TestFoldExpr(lnum) + let thisline = getline(a:lnum) + if thisline == 'a' + return 1 + elseif thisline == 'b' + return 0 + elseif thisline == 'c' + return '<1' + elseif thisline == 'd' + return '>1' + endif + return 0 + endfunction + ]]) + helpers.write_file(tempfname, [[ + b + b + a + a + d + a + a + c]]) + insert([[ + a + a + a + a + a + a + ]]) + execute('set foldmethod=expr', 'set foldexpr=TestFoldExpr(v:lnum)', '2', 'foldopen') + execute('read ' .. tempfname, '%foldclose') + eq(2, funcs.foldclosedend(1)) + eq(0, funcs.foldlevel(3)) + eq(0, funcs.foldlevel(4)) + eq(6, funcs.foldclosedend(5)) + eq(10, funcs.foldclosedend(7)) + eq(14, funcs.foldclosedend(11)) + end) end) diff --git a/test/functional/options/pastetoggle_spec.lua b/test/functional/options/pastetoggle_spec.lua new file mode 100644 index 0000000000..e449df31f5 --- /dev/null +++ b/test/functional/options/pastetoggle_spec.lua @@ -0,0 +1,37 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local feed = helpers.feed +local execute = helpers.execute +local eq = helpers.eq +local eval = helpers.eval +local sleep = helpers.sleep + +describe("'pastetoggle' option", function() + before_each(function() + clear() + execute('set nopaste') + execute('set pastetoggle=a') + end) + it("toggles 'paste'", function() + eq(eval('&paste'), 0) + feed('a') + -- Need another key so that the vgetorpeek() function returns. + feed('j') + eq(eval('&paste'), 1) + end) + it("multiple key 'pastetoggle' is waited for", function() + eq(eval('&paste'), 0) + local pastetoggle = 'lllll' + execute('set pastetoggle=' .. pastetoggle) + execute('set timeoutlen=1', 'set ttimoutlen=10000') + feed(pastetoggle:sub(0, 2)) + -- sleep() for long enough that vgetorpeek() is gotten into, but short + -- enough that ttimeoutlen is not reached. + sleep(200) + feed(pastetoggle:sub(3, -1)) + -- Need another key so that the vgetorpeek() function returns. + feed('j') + eq(eval('&paste'), 1) + end) +end) diff --git a/test/functional/shada/shada_spec.lua b/test/functional/shada/shada_spec.lua index 32598fc399..ca44026852 100644 --- a/test/functional/shada/shada_spec.lua +++ b/test/functional/shada/shada_spec.lua @@ -180,8 +180,7 @@ describe('ShaDa support code', function() nvim_command('undo') nvim_command('set shada+=%') nvim_command('wshada! ' .. shada_fname) - local readme_fname = paths.test_source_path .. '/README.md' - readme_fname = helpers.eval( 'resolve("' .. readme_fname .. '")' ) + local readme_fname = funcs.resolve(paths.test_source_path) .. '/README.md' eq({[7]=1, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) nvim_command('set shada+=r~') nvim_command('wshada! ' .. shada_fname) @@ -189,7 +188,8 @@ describe('ShaDa support code', function() nvim_command('set shada-=r~') nvim_command('wshada! ' .. shada_fname) eq({[7]=1, [8]=2, [9]=1, [10]=4, [11]=1}, find_file(readme_fname)) - nvim_command('set shada+=r' .. paths.test_source_path) + nvim_command('set shada+=r' .. funcs.escape( + funcs.escape(paths.test_source_path, '$~'), ' "\\,')) nvim_command('wshada! ' .. shada_fname) eq({}, find_file(readme_fname)) end) diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua index d990f92c3a..84f14585fa 100644 --- a/test/functional/terminal/cursor_spec.lua +++ b/test/functional/terminal/cursor_spec.lua @@ -60,16 +60,17 @@ describe('terminal cursor', function() ]]) end) - pending('is positioned correctly when focused', function() + it('is positioned correctly when focused', function() feed('i') + helpers.wait() screen:expect([[ - 1 tty ready | - 2 {1: } | - 3 | - 4 | - 5 | - 6 | - -- TERMINAL -- | + {7: 1 }tty ready | + {7: 2 }{1: } | + {7: 3 } | + {7: 4 } | + {7: 5 } | + {7: 6 } | + {3:-- TERMINAL --} | ]]) end) end) diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index 81649f2bde..4ead288a19 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -8,6 +8,7 @@ local command = helpers.command local wait = helpers.wait local retry = helpers.retry local curbufmeths = helpers.curbufmeths +local nvim = helpers.nvim local feed_data = thelpers.feed_data if helpers.pending_win32(pending) then return end @@ -368,6 +369,12 @@ describe("'scrollback' option", function() clear() end) + local function set_fake_shell() + -- shell-test.c is a fake shell that prints its arguments and exits. + nvim('set_option', 'shell', nvim_dir..'/shell-test') + nvim('set_option', 'shellcmdflag', 'EXE') + end + local function expect_lines(expected, epsilon) local ep = epsilon and epsilon or 0 local actual = eval("line('$')") @@ -421,12 +428,13 @@ describe("'scrollback' option", function() screen:detach() end) - it('defaults to 1000', function() - execute('terminal') + it('defaults to 1000 in terminal buffers', function() + set_fake_shell() + command('terminal') eq(1000, curbufmeths.get_option('scrollback')) end) - it('error if set to invalid values', function() + it('error if set to invalid value', function() local status, rv = pcall(command, 'set scrollback=-2') eq(false, status) -- assert failure eq('E474:', string.match(rv, "E%d*:")) @@ -437,15 +445,32 @@ describe("'scrollback' option", function() end) it('defaults to -1 on normal buffers', function() - execute('new') + command('new') eq(-1, curbufmeths.get_option('scrollback')) end) - it('error if set on a normal buffer', function() + it(':setlocal in a normal buffer is an error', function() command('new') - execute('set scrollback=42') + execute('setlocal scrollback=42') feed('<CR>') eq('E474:', string.match(eval("v:errmsg"), "E%d*:")) + eq(-1, curbufmeths.get_option('scrollback')) + end) + + it(':set updates local value and global default', function() + set_fake_shell() + command('set scrollback=42') -- set global and (attempt) local + eq(-1, curbufmeths.get_option('scrollback')) -- normal buffer: -1 + command('terminal') + eq(42, curbufmeths.get_option('scrollback')) -- inherits global default + command('setlocal scrollback=99') + eq(99, curbufmeths.get_option('scrollback')) + command('set scrollback<') -- reset to global default + eq(42, curbufmeths.get_option('scrollback')) + command('setglobal scrollback=734') -- new global default + eq(42, curbufmeths.get_option('scrollback')) -- local value did not change + command('terminal') + eq(734, curbufmeths.get_option('scrollback')) end) end) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 0e5c437c28..90051b8cd5 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -322,6 +322,7 @@ describe("tui 't_Co' (terminal colors)", function() helpers.nvim_prog)) thelpers.feed_data(":echo &t_Co\n") + helpers.wait() local tline if maxcolors == 8 then tline = "~ " diff --git a/test/functional/ui/cursor_spec.lua b/test/functional/ui/cursor_spec.lua new file mode 100644 index 0000000000..02e9422781 --- /dev/null +++ b/test/functional/ui/cursor_spec.lua @@ -0,0 +1,192 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear, meths = helpers.clear, helpers.meths +local eq = helpers.eq +local command = helpers.command + +describe('ui/cursor', function() + local screen + + before_each(function() + clear() + screen = Screen.new(25, 5) + screen:attach() + end) + + after_each(function() + screen:detach() + end) + + it("'guicursor' is published as a UI event", function() + local expected_cursor_style = { + cmdline_hover = { + mouse_shape = 0, + short_name = 'e' }, + cmdline_insert = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 25, + cursor_shape = 'vertical', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'ci' }, + cmdline_normal = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 0, + cursor_shape = 'block', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'c' }, + cmdline_replace = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 20, + cursor_shape = 'horizontal', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'cr' }, + insert = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 25, + cursor_shape = 'vertical', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'i' }, + more = { + mouse_shape = 0, + short_name = 'm' }, + more_lastline = { + mouse_shape = 0, + short_name = 'ml' }, + normal = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 0, + cursor_shape = 'block', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'n' }, + operator = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 50, + cursor_shape = 'horizontal', + hl_id = 46, + id_lm = 46, + mouse_shape = 0, + short_name = 'o' }, + replace = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 20, + cursor_shape = 'horizontal', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'r' }, + showmatch = { + blinkoff = 150, + blinkon = 175, + blinkwait = 175, + cell_percentage = 0, + cursor_shape = 'block', + hl_id = 46, + id_lm = 46, + short_name = 'sm' }, + statusline_drag = { + mouse_shape = 0, + short_name = 'sd' }, + statusline_hover = { + mouse_shape = 0, + short_name = 's' }, + visual = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 0, + cursor_shape = 'block', + hl_id = 46, + id_lm = 47, + mouse_shape = 0, + short_name = 'v' }, + visual_select = { + blinkoff = 250, + blinkon = 400, + blinkwait = 700, + cell_percentage = 35, + cursor_shape = 'vertical', + hl_id = 46, + id_lm = 46, + mouse_shape = 0, + short_name = 've' }, + vsep_drag = { + mouse_shape = 0, + short_name = 'vd' }, + vsep_hover = { + mouse_shape = 0, + short_name = 'vs' } + } + + screen:expect(function() + -- Default 'guicursor' published on startup. + eq(expected_cursor_style, screen._cursor_style) + eq(true, screen._cursor_style_enabled) + eq('normal', screen.mode) + end) + + -- Event is published ONLY if the cursor style changed. + screen._cursor_style = nil + command("echo 'test'") + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + test | + ]], nil, nil, function() + eq(nil, screen._cursor_style) + end) + + -- Change the cursor style. + meths.set_option('guicursor', 'n-v-c:ver35-blinkwait171-blinkoff172-blinkon173,ve:hor35,o:ver50,i-ci:block,r-cr:hor90,sm:ver42') + screen:expect(function() + eq('vertical', screen._cursor_style.normal.cursor_shape) + eq('horizontal', screen._cursor_style.visual_select.cursor_shape) + eq('vertical', screen._cursor_style.operator.cursor_shape) + eq('block', screen._cursor_style.insert.cursor_shape) + eq('vertical', screen._cursor_style.showmatch.cursor_shape) + eq(171, screen._cursor_style.normal.blinkwait) + eq(172, screen._cursor_style.normal.blinkoff) + eq(173, screen._cursor_style.normal.blinkon) + end) + end) + + it("empty 'guicursor' sets cursor_shape=block in all modes", function() + meths.set_option('guicursor', '') + screen:expect(function() + -- Empty 'guicursor' sets enabled=false. + eq(false, screen._cursor_style_enabled) + for _, m in ipairs({ 'cmdline_insert', 'cmdline_normal', 'cmdline_replace', 'insert', + 'showmatch', 'normal', 'replace', 'visual', + 'visual_select', }) do + eq('block', screen._cursor_style[m].cursor_shape) + eq(0, screen._cursor_style[m].blinkon) + end + end) + end) + +end) diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index 945b16ef92..05cf3231ea 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -200,58 +200,31 @@ describe('Default highlight groups', function() it('insert mode text', function() feed('i') + screen:try_resize(53, 4) screen:expect([[ ^ | {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| {1:-- INSERT --} | ]], {[0] = {bold=true, foreground=Screen.colors.Blue}, [1] = {bold = true}}) end) it('end of file markers', function() + screen:try_resize(53, 4) screen:expect([[ ^ | {1:~ }| {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| - {1:~ }| | ]], {[1] = {bold = true, foreground = Screen.colors.Blue}}) end) it('"wait return" text', function() + screen:try_resize(53, 4) feed(':ls<cr>') screen:expect([[ {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| :ls | 1 %a "[No Name]" line 1 | {1:Press ENTER or type command to continue}^ | @@ -259,23 +232,15 @@ describe('Default highlight groups', function() [1] = {bold = true, foreground = Screen.colors.SeaGreen}}) feed('<cr>') -- skip the "Press ENTER..." state or tests will hang end) + it('can be cleared and linked to other highlight groups', function() + screen:try_resize(53, 4) execute('highlight clear ModeMsg') feed('i') screen:expect([[ ^ | {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| -- INSERT -- | ]], {[0] = {bold=true, foreground=Screen.colors.Blue}, [1] = {bold=true}}) @@ -287,21 +252,13 @@ describe('Default highlight groups', function() ^ | {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| {1:-- INSERT --} | ]], {[0] = {bold=true, foreground=Screen.colors.Blue}, [1] = {foreground = Screen.colors.Red, background = Screen.colors.Green}}) end) + it('can be cleared by assigning NONE', function() + screen:try_resize(53, 4) execute('syn keyword TmpKeyword neovim') execute('hi link TmpKeyword ErrorMsg') insert('neovim') @@ -309,16 +266,6 @@ describe('Default highlight groups', function() {1:neovi^m} | {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| | ]], { [0] = {bold=true, foreground=Screen.colors.Blue}, @@ -330,18 +277,34 @@ describe('Default highlight groups', function() neovi^m | {0:~ }| {0:~ }| + | + ]], {[0] = {bold=true, foreground=Screen.colors.Blue}}) + end) + + it('Whitespace highlight', function() + screen:try_resize(53, 4) + execute('highlight NonText gui=NONE guifg=#FF0000') + execute('set listchars=space:.,tab:>-,trail:*,eol:¬ list') + insert(' ne \t o\tv im ') + screen:expect([[ + ne{0:.>----.}o{0:>-----}v{0:..}im{0:*^*¬} | {0:~ }| {0:~ }| + | + ]], { + [0] = {foreground=Screen.colors.Red}, + [1] = {foreground=Screen.colors.Blue}, + }) + execute('highlight Whitespace gui=NONE guifg=#0000FF') + screen:expect([[ + ne{1:.>----.}o{1:>-----}v{1:..}im{1:*^*}{0:¬} | {0:~ }| {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - {0:~ }| - | - ]], {[0] = {bold=true, foreground=Screen.colors.Blue}}) + :highlight Whitespace gui=NONE guifg=#0000FF | + ]], { + [0] = {foreground=Screen.colors.Red}, + [1] = {foreground=Screen.colors.Blue}, + }) end) end) @@ -401,7 +364,7 @@ describe('guisp (special/undercurl)', function() end) end) -describe("'cursorline' with 'listchars'", function() +describe("'listchars' highlight", function() local screen before_each(function() @@ -510,7 +473,7 @@ describe("'cursorline' with 'listchars'", function() }, }) execute('highlight clear ModeMsg') - execute('highlight SpecialKey guifg=#FF0000') + execute('highlight Whitespace guifg=#FF0000') execute('set cursorline') execute('set tabstop=8') execute('set listchars=space:.,eol:¬,tab:>-,extends:>,precedes:<,trail:* list') @@ -606,7 +569,7 @@ describe("'cursorline' with 'listchars'", function() }, }) execute('highlight clear ModeMsg') - execute('highlight SpecialKey guifg=#FF0000') + execute('highlight Whitespace guifg=#FF0000') execute('set cursorline') execute('set tabstop=8') execute('set nowrap') @@ -644,4 +607,41 @@ describe("'cursorline' with 'listchars'", function() | ]]) end) + + it("'cursorline' with :match", function() + screen:set_default_attr_ids({ + [0] = {bold=true, foreground=Screen.colors.Blue}, + [1] = {background=Screen.colors.Grey90}, + [2] = {foreground=Screen.colors.Red}, + [3] = {foreground=Screen.colors.Green1}, + }) + execute('highlight clear ModeMsg') + execute('highlight Whitespace guifg=#FF0000') + execute('highlight Error guifg=#00FF00') + execute('set nowrap') + feed('ia \t bc \t <esc>') + screen:expect([[ + a bc ^ | + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + execute('set listchars=space:.,eol:¬,tab:>-,extends:>,precedes:<,trail:* list') + screen:expect([[ + a{2:.>-----.}bc{2:*>---*^*}{0:¬} | + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + execute('match Error /\\s\\+$/') + screen:expect([[ + a{2:.>-----.}bc{3:*>---*^*}{0:¬} | + {0:~ }| + {0:~ }| + {0:~ }| + | + ]]) + end) end) diff --git a/test/functional/ui/mouse_spec.lua b/test/functional/ui/mouse_spec.lua index b2fbedfb5e..ecbd5642d1 100644 --- a/test/functional/ui/mouse_spec.lua +++ b/test/functional/ui/mouse_spec.lua @@ -6,7 +6,7 @@ local eq, funcs = helpers.eq, helpers.funcs if helpers.pending_win32(pending) then return end -describe('Mouse input', function() +describe('ui/mouse/input', function() local screen before_each(function() diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index 54f43387dc..2f2cc85dab 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -181,6 +181,7 @@ end -- expected: Expected screen state (string). Each line represents a screen -- row. Last character of each row (typically "|") is stripped. -- Common indentation is stripped. +-- Used as `condition` if NOT a string; must be the ONLY arg then. -- attr_ids: Expected text attributes. Screen rows are transformed according -- to this table, as follows: each substring S composed of -- characters having the same attributes will be substituted by @@ -191,18 +192,23 @@ end -- any: true: Succeed if `expected` matches ANY screen line(s). -- false (default): `expected` must match screen exactly. function Screen:expect(expected, attr_ids, attr_ignore, condition, any) - -- remove the last line and dedent - expected = dedent(expected:gsub('\n[ ]+$', '')) local expected_rows = {} - for row in expected:gmatch('[^\n]+') do - -- the last character should be the screen delimiter - row = row:sub(1, #row - 1) - table.insert(expected_rows, row) - end - if not any then - assert(self._height == #expected_rows, - "Expected screen state's row count(" .. #expected_rows - .. ') differs from configured height(' .. self._height .. ') of Screen.') + if type(expected) ~= "string" then + assert(not (attr_ids or attr_ignore or condition or any)) + condition = expected + expected = nil + else + -- Remove the last line and dedent. + expected = dedent(expected:gsub('\n[ ]+$', '')) + for row in expected:gmatch('[^\n]+') do + row = row:sub(1, #row - 1) -- Last char must be the screen delimiter. + table.insert(expected_rows, row) + end + if not any then + assert(self._height == #expected_rows, + "Expected screen state's row count(" .. #expected_rows + .. ') differs from configured height(' .. self._height .. ') of Screen.') + end end local ids = attr_ids or self._default_attr_ids local ignore = attr_ignore or self._default_attr_ignore @@ -218,7 +224,9 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition, any) actual_rows[i] = self:_row_repr(self._rows[i], ids, ignore) end - if any then + if expected == nil then + return + elseif any then -- Search for `expected` anywhere in the screen lines. local actual_screen_str = table.concat(actual_rows, '\n') if nil == string.find(actual_screen_str, expected) then @@ -313,6 +321,8 @@ function Screen:_redraw(updates) if handler ~= nil then handler(self, unpack(update[i])) else + assert(self._on_event, + "Add Screen:_handle_XXX method or call Screen:set_on_event_handler") self._on_event(method, update[i]) end end @@ -343,6 +353,11 @@ function Screen:_handle_resize(width, height) } end +function Screen:_handle_cursor_style_set(enabled, style) + self._cursor_style_enabled = enabled + self._cursor_style = style +end + function Screen:_handle_clear() self:_clear_block(self._scroll_region.top, self._scroll_region.bot, self._scroll_region.left, self._scroll_region.right) diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua index e511234e5e..21953ba294 100644 --- a/test/functional/ui/screen_basic_spec.lua +++ b/test/functional/ui/screen_basic_spec.lua @@ -73,33 +73,29 @@ describe('Screen', function() describe(':suspend', function() it('is forwarded to the UI', function() local function check() - if not screen.suspended then - return 'Screen was not suspended' - end + eq(true, screen.suspended) end execute('suspend') - screen:wait(check) + screen:expect(check) screen.suspended = false feed('<c-z>') - screen:wait(check) + screen:expect(check) end) end) describe('bell/visual bell', function() it('is forwarded to the UI', function() feed('<left>') - screen:wait(function() - if not screen.bell or screen.visual_bell then - return 'Bell was not sent' - end + screen:expect(function() + eq(true, screen.bell) + eq(false, screen.visual_bell) end) screen.bell = false execute('set visualbell') feed('<left>') - screen:wait(function() - if not screen.visual_bell or screen.bell then - return 'Visual bell was not sent' - end + screen:expect(function() + eq(true, screen.visual_bell) + eq(false, screen.bell) end) end) end) @@ -109,22 +105,16 @@ describe('Screen', function() local expected = 'test-title' execute('set titlestring='..expected) execute('set title') - screen:wait(function() - local actual = screen.title - if actual ~= expected then - return 'Expected title to be "'..expected..'" but was "'..actual..'"' - end + screen:expect(function() + eq(expected, screen.title) end) end) it('has correct default title with unnamed file', function() local expected = '[No Name] - NVIM' execute('set title') - screen:wait(function() - local actual = screen.title - if actual ~= expected then - return 'Expected title to be "'..expected..'" but was "'..actual..'"' - end + screen:expect(function() + eq(expected, screen.title) end) end) @@ -132,11 +122,8 @@ describe('Screen', function() local expected = 'myfile (/mydir) - NVIM' execute('set title') execute('file /mydir/myfile') - screen:wait(function() - local actual = screen.title - if actual ~= expected then - return 'Expected title to be "'..expected..'" but was "'..actual..'"' - end + screen:expect(function() + eq(expected, screen.title) end) end) end) @@ -146,11 +133,8 @@ describe('Screen', function() local expected = 'test-icon' execute('set iconstring='..expected) execute('set icon') - screen:wait(function() - local actual = screen.icon - if actual ~= expected then - return 'Expected title to be "'..expected..'" but was "'..actual..'"' - end + screen:expect(function() + eq(expected, screen.icon) end) end) end) diff --git a/test/functional/viml/completion_spec.lua b/test/functional/viml/completion_spec.lua index cd5f4260e0..3c09d71eb7 100644 --- a/test/functional/viml/completion_spec.lua +++ b/test/functional/viml/completion_spec.lua @@ -3,6 +3,7 @@ local Screen = require('test.functional.ui.screen') local clear, feed = helpers.clear, helpers.feed local eval, eq, neq = helpers.eval, helpers.eq, helpers.neq local execute, source, expect = helpers.execute, helpers.source, helpers.expect +local meths = helpers.meths if helpers.pending_win32(pending) then return end @@ -814,6 +815,41 @@ describe('completion', function() end) end) + describe('with numeric items', function() + before_each(function() + source([[ + function! TestComplete() abort + call complete(1, g:_complist) + return '' + endfunction + ]]) + meths.set_option('completeopt', 'menuone,noselect') + meths.set_var('_complist', {{ + word=0, + abbr=1, + menu=2, + kind=3, + info=4, + icase=5, + dup=6, + empty=7, + }}) + end) + + it('shows correct variant as word', function() + feed('i<C-r>=TestComplete()<CR>') + screen:expect([[ + ^ | + {1:1 3 2 }{0: }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + {3:-- INSERT --} | + ]]) + end) + end) end) describe('External completion popupmenu', function() diff --git a/test/helpers.lua b/test/helpers.lua index e5224349c2..3fc10e9e30 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -106,20 +106,33 @@ local uname = (function() end) end)() -local function tmpname() - local fname = os.tmpname() - if uname() == 'Windows' and fname:sub(1, 2) == '\\s' then - -- In Windows tmpname() returns a filename starting with - -- special sequence \s, prepend $TEMP path - local tmpdir = os.getenv('TEMP') - return tmpdir..fname - elseif fname:match('^/tmp') and uname() == 'Darwin' then - -- In OS X /tmp links to /private/tmp - return '/private'..fname - else - return fname - end -end +local tmpname = (function() + local seq = 0 + local tmpdir = os.getenv('TMPDIR') and os.getenv('TMPDIR') or os.getenv('TEMP') + -- Is $TMPDIR defined local to the project workspace? + local in_workspace = not not (tmpdir and string.find(tmpdir, 'Xtest')) + return (function() + if in_workspace then + -- Cannot control os.tmpname() dir, so hack our own tmpname() impl. + seq = seq + 1 + local fname = tmpdir..'/nvim-test-lua-'..seq + io.open(fname, 'w'):close() + return fname + else + local fname = os.tmpname() + if uname() == 'Windows' and fname:sub(1, 2) == '\\s' then + -- In Windows tmpname() returns a filename starting with + -- special sequence \s, prepend $TEMP path + return tmpdir..fname + elseif fname:match('^/tmp') and uname() == 'Darwin' then + -- In OS X /tmp links to /private/tmp + return '/private'..fname + else + return fname + end + end + end) +end)() local function map(func, tab) local rettab = {} @@ -225,6 +238,41 @@ local function which(exe) end end +local function concat_tables(...) + local ret = {} + for i = 1, select('#', ...) do + local tbl = select(i, ...) + if tbl then + for _, v in ipairs(tbl) do + ret[#ret + 1] = v + end + end + end + return ret +end + +local function dedent(str) + -- find minimum common indent across lines + local indent = nil + for line in str:gmatch('[^\n]+') do + local line_indent = line:match('^%s+') or '' + if indent == nil or #line_indent < #indent then + indent = line_indent + end + end + if indent == nil or #indent == 0 then + -- no minimum common indent + return str + end + -- create a pattern for the indent + indent = indent:gsub('%s', '[ \t]') + -- strip it from the first line + str = str:gsub('^'..indent, '') + -- strip it from the remaining lines + str = str:gsub('[\n]'..indent, '\n') + return str +end + return { eq = eq, neq = neq, @@ -238,4 +286,6 @@ return { check_cores = check_cores, hasenv = hasenv, which = which, + concat_tables = concat_tables, + dedent = dedent, } diff --git a/test/unit/eval/decode_spec.lua b/test/unit/eval/decode_spec.lua index 2d7597c0f4..0b2a423cd6 100644 --- a/test/unit/eval/decode_spec.lua +++ b/test/unit/eval/decode_spec.lua @@ -7,7 +7,7 @@ local eq = helpers.eq local neq = helpers.neq local ffi = helpers.ffi -local decode = cimport('./src/nvim/eval/decode.h', './src/nvim/eval_defs.h', +local decode = cimport('./src/nvim/eval/decode.h', './src/nvim/eval/typval.h', './src/nvim/globals.h', './src/nvim/memory.h', './src/nvim/message.h') diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index 1377d5b501..fa76113756 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -5,13 +5,14 @@ local to_cstr = helpers.to_cstr local ffi = helpers.ffi local eq = helpers.eq -local eval = cimport('./src/nvim/eval.h', './src/nvim/eval_defs.h', +local eval = cimport('./src/nvim/eval.h', './src/nvim/eval/typval.h', './src/nvim/hashtab.h') local null_string = {[true]='NULL string'} local null_list = {[true]='NULL list'} local null_dict = {[true]='NULL dict'} local type_key = {[true]='type key'} +local locks_key = {[true]='locks key'} local list_type = {[true]='list type'} local dict_type = {[true]='dict type'} local func_type = {[true]='func type'} @@ -23,27 +24,71 @@ local nil_value = {[true]='nil'} local lua2typvalt local function li_alloc(nogc) - local gcfunc = eval.listitem_free + local gcfunc = eval.tv_list_item_free if nogc then gcfunc = nil end - local li = ffi.gc(eval.listitem_alloc(), gcfunc) + local li = ffi.gc(eval.tv_list_item_alloc(), gcfunc) li.li_next = nil li.li_prev = nil li.li_tv = {v_type=eval.VAR_UNKNOWN, v_lock=eval.VAR_UNLOCKED} return li end -local function list(...) - local ret = ffi.gc(eval.list_alloc(), eval.list_unref) - eq(0, ret.lv_refcount) - ret.lv_refcount = 1 - for i = 1, select('#', ...) do - local val = select(i, ...) - local li_tv = ffi.gc(lua2typvalt(val), nil) - local li = li_alloc(true) - li.li_tv = li_tv - eval.tv_list_append(ret, li) +local function populate_list(l, lua_l, processed) + processed = processed or {} + eq(0, l.lv_refcount) + l.lv_refcount = 1 + processed[lua_l] = l + for i = 1, #lua_l do + local item_tv = ffi.gc(lua2typvalt(lua_l[i], processed), nil) + local item_li = eval.tv_list_item_alloc() + item_li.li_tv = item_tv + eval.tv_list_append(l, item_li) end - return ret + return l +end + +local function populate_dict(d, lua_d, processed) + processed = processed or {} + eq(0, d.dv_refcount) + d.dv_refcount = 1 + processed[lua_d] = d + for k, v in pairs(lua_d) do + if type(k) == 'string' then + local di = eval.tv_dict_item_alloc(to_cstr(k)) + local val_tv = ffi.gc(lua2typvalt(v, processed), nil) + eval.tv_copy(val_tv, di.di_tv) + eval.tv_clear(val_tv) + eval.tv_dict_add(d, di) + end + end + return d +end + +local function populate_partial(pt, lua_pt, processed) + processed = processed or {} + eq(0, pt.pt_refcount) + processed[lua_pt] = pt + local argv = nil + if lua_pt.args and #lua_pt.args > 0 then + argv = ffi.gc(ffi.cast('typval_T*', eval.xmalloc(ffi.sizeof('typval_T') * #lua_pt.args)), nil) + for i, arg in ipairs(lua_pt.args) do + local arg_tv = ffi.gc(lua2typvalt(arg, processed), nil) + argv[i - 1] = arg_tv + end + end + local dict = nil + if lua_pt.dict then + local dict_tv = ffi.gc(lua2typvalt(lua_pt.dict, processed), nil) + assert(dict_tv.v_type == eval.VAR_DICT) + dict = dict_tv.vval.v_dict + end + pt.pt_refcount = 1 + pt.pt_name = eval.xmemdupz(to_cstr(lua_pt.value), #lua_pt.value) + pt.pt_auto = not not lua_pt.auto + pt.pt_argc = lua_pt.args and #lua_pt.args or 0 + pt.pt_argv = argv + pt.pt_dict = dict + return pt end local ptr2key = function(ptr) @@ -54,6 +99,30 @@ local lst2tbl local dct2tbl local typvalt2lua + +local function partial2lua(pt, processed) + processed = processed or {} + local value, auto, dict, argv = nil, nil, nil, nil + if pt ~= nil then + value = ffi.string(pt.pt_name) + auto = pt.pt_auto and true or nil + argv = {} + for i = 1, pt.pt_argc do + argv[i] = typvalt2lua(pt.pt_argv[i - 1], processed) + end + if pt.pt_dict ~= nil then + dict = dct2tbl(pt.pt_dict) + end + end + return { + [type_key]=func_type, + value=value, + auto=auto, + args=argv, + dict=dict, + } +end + local typvalt2lua_tab = nil local function typvalt2lua_tab_init() @@ -63,10 +132,10 @@ local function typvalt2lua_tab_init() typvalt2lua_tab = { [tonumber(eval.VAR_SPECIAL)] = function(t) return ({ - [eval.kSpecialVarFalse] = false, - [eval.kSpecialVarNull] = nil_value, - [eval.kSpecialVarTrue] = true, - })[t.vval.v_special] + [tonumber(eval.kSpecialVarFalse)] = false, + [tonumber(eval.kSpecialVarNull)] = nil_value, + [tonumber(eval.kSpecialVarTrue)] = true, + })[tonumber(t.vval.v_special)] end, [tonumber(eval.VAR_NUMBER)] = function(t) return {[type_key]=int_type, value=tonumber(t.vval.v_number)} @@ -96,26 +165,7 @@ local function typvalt2lua_tab_init() if processed[p_key] then return processed[p_key] end - local pt = t.vval.v_partial - local value, auto, dict, argv = nil, nil, nil, nil - if pt ~= nil then - value = ffi.string(pt.pt_name) - auto = pt.pt_auto and true or nil - argv = {} - for i = 1, pt.pt_argc do - argv[i] = typvalt2lua(pt.pt_argv[i - 1], processed) - end - if pt.pt_dict ~= nil then - dict = dct2tbl(pt.pt_dict) - end - end - return { - [type_key]=func_type, - value=value, - auto=auto, - args=argv, - dict=dict, - } + return partial2lua(t.vval.v_partial, processed) end, } end @@ -241,7 +291,7 @@ local typvalt = function(typ, vval) elseif type(typ) == 'string' then typ = eval[typ] end - return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv) + return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.tv_clear) end local lua2typvalt_type_tab = { @@ -256,36 +306,16 @@ local lua2typvalt_type_tab = { processed[l].lv_refcount = processed[l].lv_refcount + 1 return typvalt(eval.VAR_LIST, {v_list=processed[l]}) end - local lst = eval.list_alloc() - lst.lv_refcount = 1 - processed[l] = lst - local ret = typvalt(eval.VAR_LIST, {v_list=lst}) - for i = 1, #l do - local item_tv = ffi.gc(lua2typvalt(l[i], processed), nil) - eval.list_append_tv(lst, item_tv) - eval.clear_tv(item_tv) - end - return ret + local lst = populate_list(eval.tv_list_alloc(), l, processed) + return typvalt(eval.VAR_LIST, {v_list=lst}) end, [dict_type] = function(l, processed) if processed[l] then processed[l].dv_refcount = processed[l].dv_refcount + 1 return typvalt(eval.VAR_DICT, {v_dict=processed[l]}) end - local dct = eval.dict_alloc() - dct.dv_refcount = 1 - processed[l] = dct - local ret = typvalt(eval.VAR_DICT, {v_dict=dct}) - for k, v in pairs(l) do - if type(k) == 'string' then - local di = eval.dictitem_alloc(to_cstr(k)) - local val_tv = ffi.gc(lua2typvalt(v, processed), nil) - eval.copy_tv(val_tv, di.di_tv) - eval.clear_tv(val_tv) - eval.dict_add(dct, di) - end - end - return ret + local dct = populate_dict(eval.tv_dict_alloc(), l, processed) + return typvalt(eval.VAR_DICT, {v_dict=dct}) end, [func_type] = function(l, processed) if processed[l] then @@ -293,29 +323,8 @@ local lua2typvalt_type_tab = { return typvalt(eval.VAR_PARTIAL, {v_partial=processed[l]}) end if l.args or l.dict then - local pt = ffi.gc(ffi.cast('partial_T*', eval.xmalloc(ffi.sizeof('partial_T'))), nil) - processed[l] = pt - local argv = nil - if l.args and #l.args > 0 then - argv = ffi.gc(ffi.cast('typval_T*', eval.xmalloc(ffi.sizeof('typval_T') * #l.args)), nil) - for i, arg in ipairs(l.args) do - local arg_tv = ffi.gc(lua2typvalt(arg, processed), nil) - eval.copy_tv(arg_tv, argv[i - 1]) - eval.clear_tv(arg_tv) - end - end - local dict = nil - if l.dict then - local dict_tv = ffi.gc(lua2typvalt(l.dict, processed), nil) - assert(dict_tv.v_type == eval.VAR_DICT) - dict = dict_tv.vval.v_dict - end - pt.pt_refcount = 1 - pt.pt_name = eval.xmemdupz(to_cstr(l.value), #l.value) - pt.pt_auto = not not l.auto - pt.pt_argc = l.args and #l.args or 0 - pt.pt_argv = argv - pt.pt_dict = dict + local pt = populate_partial(ffi.gc(ffi.cast('partial_T*', + eval.xcalloc(1, ffi.sizeof('partial_T'))), nil), l, processed) return typvalt(eval.VAR_PARTIAL, {v_partial=pt}) else return typvalt(eval.VAR_FUNC, { @@ -368,13 +377,24 @@ lua2typvalt = function(l, processed) return typvalt(eval.VAR_STRING, {v_string=eval.xmemdupz(to_cstr(l), #l)}) elseif type(l) == 'cdata' then local tv = typvalt(eval.VAR_UNKNOWN) - eval.copy_tv(l, tv) + eval.tv_copy(l, tv) return tv end end +local void_ptr = ffi.typeof('void *') local function void(ptr) - return ffi.cast('void*', ptr) + return ffi.cast(void_ptr, ptr) +end + +local function alloc_len(len, get_ptr) + if type(len) == 'string' or type(len) == 'table' then + return #len + elseif len == nil then + return eval.strlen(get_ptr()) + else + return len + end end local alloc_logging_helpers = { @@ -382,14 +402,115 @@ local alloc_logging_helpers = { li = function(li) return {func='malloc', args={ffi.sizeof('listitem_T')}, ret=void(li)} end, dict = function(d) return {func='malloc', args={ffi.sizeof('dict_T')}, ret=void(d)} end, di = function(di, size) + size = alloc_len(size, function() return di.di_key end) return {func='malloc', args={ffi.offsetof('dictitem_T', 'di_key') + size + 1}, ret=void(di)} end, - str = function(s, size) return {func='malloc', args={size + 1}, ret=void(s)} end, + str = function(s, size) + size = alloc_len(size, function() return s end) + return {func='malloc', args={size + 1}, ret=void(s)} + end, + + dwatcher = function(w) return {func='malloc', args={ffi.sizeof('DictWatcher')}, ret=void(w)} end, - freed = function(p) return {func='free', args={p and void(p)}} end, + freed = function(p) return {func='free', args={type(p) == 'table' and p or void(p)}} end, + + -- lua_…: allocated by this file, not by some Neovim function + lua_pt = function(pt) return {func='calloc', args={1, ffi.sizeof('partial_T')}, ret=void(pt)} end, + lua_tvs = function(argv, argc) + argc = alloc_len(argc) + return {func='malloc', args={ffi.sizeof('typval_T')*argc}, ret=void(argv)} + end, } +local function int(n) + return {[type_key]=int_type, value=n} +end + +local function list(...) + return populate_list(ffi.gc(eval.tv_list_alloc(), eval.tv_list_unref), + {...}, {}) +end + +local function dict(d) + return populate_dict(ffi.gc(eval.tv_dict_alloc(), eval.tv_dict_free), + d or {}, {}) +end + +local callback2tbl_type_tab = nil + +local function init_callback2tbl_type_tab() + if callback2tbl_type_tab then + return + end + callback2tbl_type_tab = { + [tonumber(eval.kCallbackNone)] = function(_) return {type='none'} end, + [tonumber(eval.kCallbackFuncref)] = function(cb) + return {type='fref', fref=ffi.string(cb.data.funcref)} + end, + [tonumber(eval.kCallbackPartial)] = function(cb) + local lua_pt = partial2lua(cb.data.partial) + return {type='pt', fref=ffi.string(lua_pt.value), pt=lua_pt} + end + } +end + +local function callback2tbl(cb) + init_callback2tbl_type_tab() + return callback2tbl_type_tab[tonumber(cb.type)](cb) +end + +local function tbl2callback(tbl) + local ret = nil + if tbl.type == 'none' then + ret = ffi.new('Callback[1]', {{type=eval.kCallbackNone}}) + elseif tbl.type == 'fref' then + ret = ffi.new('Callback[1]', {{type=eval.kCallbackFuncref, + data={funcref=eval.xstrdup(tbl.fref)}}}) + elseif tbl.type == 'pt' then + local pt = ffi.gc(ffi.cast('partial_T*', + eval.xcalloc(1, ffi.sizeof('partial_T'))), eval.partial_unref) + ret = ffi.new('Callback[1]', {{type=eval.kCallbackPartial, + data={partial=populate_partial(pt, tbl.pt, {})}}}) + else + assert(false) + end + return ffi.gc(ffi.cast('Callback*', ret), helpers.callback_free) +end + +local function dict_watchers(d) + local ret = {} + local h = d.watchers + local q = h.next + local qs = {} + local key_patterns = {} + while q ~= h do + local qitem = ffi.cast('DictWatcher *', + ffi.cast('char *', q) - ffi.offsetof('DictWatcher', 'node')) + ret[#ret + 1] = { + cb=callback2tbl(qitem.callback), + pat=ffi.string(qitem.key_pattern, qitem.key_pattern_len), + busy=qitem.busy, + } + qs[#qs + 1] = qitem + key_patterns[#key_patterns + 1] = {qitem.key_pattern, qitem.key_pattern_len} + q = q.next + end + return ret, qs, key_patterns +end + +local function eval0(expr) + local tv = ffi.gc(ffi.new('typval_T', {v_type=eval.VAR_UNKNOWN}), + eval.tv_clear) + if eval.eval0(to_cstr(expr), tv, nil, true) == 0 then + return nil + else + return tv + end +end + return { + int=int, + null_string=null_string, null_list=null_list, null_dict=null_dict, @@ -402,8 +523,10 @@ return { nil_value=nil_value, type_key=type_key, + locks_key=locks_key, list=list, + dict=dict, lst2tbl=lst2tbl, dct2tbl=dct2tbl, @@ -422,4 +545,12 @@ return { list_items=list_items, dict_items=dict_items, + + dict_watchers=dict_watchers, + tbl2callback=tbl2callback, + callback2tbl=callback2tbl, + + eval0=eval0, + + empty_list = {[type_key]=list_type}, } diff --git a/test/unit/eval/tricks_spec.lua b/test/unit/eval/tricks_spec.lua index ec79a9cad5..7aa0f0f6e6 100644 --- a/test/unit/eval/tricks_spec.lua +++ b/test/unit/eval/tricks_spec.lua @@ -1,19 +1,15 @@ local helpers = require('test.unit.helpers')(after_each) +local eval_helpers = require('test.unit.eval.helpers') + local itp = helpers.gen_itp(it) local cimport = helpers.cimport -local to_cstr = helpers.to_cstr -local ffi = helpers.ffi local eq = helpers.eq -local eval = cimport('./src/nvim/eval.h', './src/nvim/memory.h') +local eval0 = eval_helpers.eval0 -local eval_expr = function(expr) - return ffi.gc(eval.eval_expr(to_cstr(expr), nil), function(tv) - eval.clear_tv(tv) - eval.xfree(tv) - end) -end +local eval = cimport('./src/nvim/eval.h', './src/nvim/eval/typval.h', + './src/nvim/memory.h') describe('NULL typval_T', function() itp('is produced by $XXX_UNEXISTENT_VAR_XXX', function() @@ -25,19 +21,19 @@ describe('NULL typval_T', function() while os.getenv(unexistent_env) ~= nil do unexistent_env = unexistent_env .. '_XXX' end - local rettv = eval_expr('$' .. unexistent_env) + local rettv = eval0('$' .. unexistent_env) eq(eval.VAR_STRING, rettv.v_type) eq(nil, rettv.vval.v_string) end) itp('is produced by v:_null_list', function() - local rettv = eval_expr('v:_null_list') + local rettv = eval0('v:_null_list') eq(eval.VAR_LIST, rettv.v_type) eq(nil, rettv.vval.v_list) end) itp('is produced by v:_null_dict', function() - local rettv = eval_expr('v:_null_dict') + local rettv = eval0('v:_null_dict') eq(eval.VAR_DICT, rettv.v_type) eq(nil, rettv.vval.v_dict) end) diff --git a/test/unit/eval/tv_clear_spec.lua b/test/unit/eval/tv_clear_spec.lua index 47d4661ad8..ca37301b32 100644 --- a/test/unit/eval/tv_clear_spec.lua +++ b/test/unit/eval/tv_clear_spec.lua @@ -14,7 +14,7 @@ local list_items = eval_helpers.list_items local dict_items = eval_helpers.dict_items local lua2typvalt = eval_helpers.lua2typvalt -local lib = cimport('./src/nvim/eval_defs.h', './src/nvim/eval.h') +local lib = cimport('./src/nvim/eval/typval.h', './src/nvim/eval.h') local alloc_log = alloc_log_new() @@ -26,7 +26,7 @@ after_each(function() alloc_log:after_each() end) -describe('clear_tv()', function() +describe('tv_clear()', function() itp('successfully frees all lists in [&l [1], *l, *l]', function() local l_inner = {1} local list = {l_inner, l_inner, l_inner} @@ -44,7 +44,7 @@ describe('clear_tv()', function() a.li(lis[3]), }) eq(3, list_inner_p.lv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(lis_inner[1]), a.freed(list_inner_p), @@ -69,7 +69,7 @@ describe('clear_tv()', function() a.li(lis[3]), }) eq(3, list_inner_p.lv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(list_inner_p), a.freed(lis[1]), @@ -92,7 +92,7 @@ describe('clear_tv()', function() a.li(lis[2]), }) eq(2, dict_inner_p.dv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(dict_inner_p), a.freed(lis[1]), @@ -116,7 +116,7 @@ describe('clear_tv()', function() a.li(lis[2]), }) eq(2, dict_inner_p.dv_refcount) - lib.clear_tv(list_tv) + lib.tv_clear(list_tv) alloc_log:check({ a.freed(dis.a), a.freed(dict_inner_p), diff --git a/test/unit/eval/typval_spec.lua b/test/unit/eval/typval_spec.lua new file mode 100644 index 0000000000..258b5c4c1f --- /dev/null +++ b/test/unit/eval/typval_spec.lua @@ -0,0 +1,2933 @@ +local bit = require('bit') +local helpers = require('test.unit.helpers')(after_each) +local eval_helpers = require('test.unit.eval.helpers') +local global_helpers = require('test.helpers') + +local itp = helpers.gen_itp(it) + +local OK = helpers.OK +local eq = helpers.eq +local neq = helpers.neq +local ffi = helpers.ffi +local FAIL = helpers.FAIL +local NULL = helpers.NULL +local cimport = helpers.cimport +local to_cstr = helpers.to_cstr +local alloc_log_new = helpers.alloc_log_new + +local a = eval_helpers.alloc_logging_helpers +local int = eval_helpers.int +local list = eval_helpers.list +local dict = eval_helpers.dict +local eval0 = eval_helpers.eval0 +local lst2tbl = eval_helpers.lst2tbl +local dct2tbl = eval_helpers.dct2tbl +local typvalt = eval_helpers.typvalt +local type_key = eval_helpers.type_key +local li_alloc = eval_helpers.li_alloc +local first_di = eval_helpers.first_di +local nil_value = eval_helpers.nil_value +local func_type = eval_helpers.func_type +local null_list = eval_helpers.null_list +local null_dict = eval_helpers.null_dict +local dict_items = eval_helpers.dict_items +local list_items = eval_helpers.list_items +local empty_list = eval_helpers.empty_list +local lua2typvalt = eval_helpers.lua2typvalt +local typvalt2lua = eval_helpers.typvalt2lua +local null_string = eval_helpers.null_string +local callback2tbl = eval_helpers.callback2tbl +local tbl2callback = eval_helpers.tbl2callback +local dict_watchers = eval_helpers.dict_watchers + +local concat_tables = global_helpers.concat_tables + +local lib = cimport('./src/nvim/eval/typval.h', './src/nvim/memory.h', + './src/nvim/mbyte.h', './src/nvim/garray.h', + './src/nvim/eval.h', './src/nvim/vim.h', + './src/nvim/globals.h') + +local function list_watch_alloc(li) + return ffi.cast('listwatch_T*', ffi.new('listwatch_T[1]', {{lw_item=li}})) +end + +local function list_watch(l, li) + local lw = list_watch_alloc(li or l.lv_first) + lib.tv_list_watch_add(l, lw) + return lw +end + +local function get_alloc_rets(exp_log, res) + setmetatable(res, { + __index={ + freed=function(r, n) return {func='free', args={r[n]}} end + } + }) + for i = 1,#exp_log do + if ({malloc=true, calloc=true})[exp_log[i].func] then + res[#res + 1] = exp_log[i].ret + end + end + return exp_log +end + +local to_cstr_nofree = function(v) return lib.xstrdup(v) end + +local alloc_log = alloc_log_new() + +before_each(function() + alloc_log:before_each() +end) + +after_each(function() + alloc_log:after_each() +end) + +local function ga_alloc(itemsize, growsize) + local ga = ffi.gc(ffi.cast('garray_T*', ffi.new('garray_T[1]', {})), + lib.ga_clear) + lib.ga_init(ga, itemsize or 1, growsize or 80) + return ga +end + +local function check_emsg(f, msg) + local saved_last_msg_hist = lib.last_msg_hist + if saved_last_msg_hist == nil then + saved_last_msg_hist = nil + end + local ret = {f()} + if msg ~= nil then + eq(msg, ffi.string(lib.last_msg_hist.msg)) + neq(saved_last_msg_hist, lib.last_msg_hist) + else + if saved_last_msg_hist ~= lib.last_msg_hist then + eq(nil, ffi.string(lib.last_msg_hist.msg)) + else + eq(saved_last_msg_hist, lib.last_msg_hist) + end + end + return unpack(ret) +end + +describe('typval.c', function() + describe('list', function() + describe('item', function() + describe('alloc()/free()', function() + itp('works', function() + local li = li_alloc(true) + neq(nil, li) + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + end) + itp('also frees the value', function() + local li + local s + local l + local tv + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_NUMBER + li.li_tv.vval.v_number = 10 + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_FLOAT + li.li_tv.vval.v_float = 10.5 + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_STRING + li.li_tv.vval.v_string = nil + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.freed(alloc_log.null), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_STRING + s = to_cstr_nofree('test') + li.li_tv.vval.v_string = s + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.str(s, #('test')), + a.freed(s), + a.freed(li), + }) + + li = li_alloc(true) + li.li_tv.v_type = lib.VAR_LIST + l = ffi.gc(list(), nil) + l.lv_refcount = 2 + li.li_tv.vval.v_list = l + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.list(l), + a.freed(li), + }) + eq(1, l.lv_refcount) + + li = li_alloc(true) + tv = lua2typvalt({}) + tv.vval.v_dict.dv_refcount = 2 + li.li_tv = tv + lib.tv_list_item_free(li) + alloc_log:check({ + a.li(li), + a.dict(tv.vval.v_dict), + a.freed(li), + }) + eq(1, tv.vval.v_dict.dv_refcount) + end) + end) + describe('remove()', function() + itp('works', function() + local l = list(1, 2, 3, 4, 5, 6, 7) + neq(nil, l) + local lis = list_items(l) + alloc_log:check({ + a.list(l), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + a.li(lis[4]), + a.li(lis[5]), + a.li(lis[6]), + a.li(lis[7]), + }) + + lib.tv_list_item_remove(l, lis[1]) + alloc_log:check({ + a.freed(table.remove(lis, 1)), + }) + eq(lis, list_items(l)) + + lib.tv_list_item_remove(l, lis[6]) + alloc_log:check({ + a.freed(table.remove(lis)), + }) + eq(lis, list_items(l)) + + lib.tv_list_item_remove(l, lis[3]) + alloc_log:check({ + a.freed(table.remove(lis, 3)), + }) + eq(lis, list_items(l)) + end) + itp('works and adjusts watchers correctly', function() + local l = ffi.gc(list(1, 2, 3, 4, 5, 6, 7), nil) + neq(nil, l) + local lis = list_items(l) + -- Three watchers: pointing to first, middle and last elements. + local lws = { + list_watch(l, lis[1]), + list_watch(l, lis[4]), + list_watch(l, lis[7]), + } + + lib.tv_list_item_remove(l, lis[4]) + ffi.gc(lis[4], lib.tv_list_item_free) + eq({lis[1], lis[5], lis[7]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_item_remove(l, lis[2]) + ffi.gc(lis[2], lib.tv_list_item_free) + eq({lis[1], lis[5], lis[7]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_item_remove(l, lis[7]) + ffi.gc(lis[7], lib.tv_list_item_free) + eq({lis[1], lis[5], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_item_remove(l, lis[1]) + ffi.gc(lis[1], lib.tv_list_item_free) + eq({lis[3], lis[5], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + alloc_log:clear() + lib.tv_list_watch_remove(l, lws[2]) + lib.tv_list_watch_remove(l, lws[3]) + lib.tv_list_watch_remove(l, lws[1]) + lib.tv_list_free(l) + alloc_log:check({ + a.freed(lis[3]), + a.freed(lis[5]), + a.freed(lis[6]), + a.freed(l), + }) + end) + end) + end) + describe('watch', function() + describe('remove()', function() + itp('works', function() + local l = ffi.gc(list(1, 2, 3, 4, 5, 6, 7), nil) + eq(nil, l.lv_watch) + local lw = list_watch(l) + neq(nil, l.lv_watch) + alloc_log:clear() + lib.tv_list_watch_remove(l, lw) + eq(nil, l.lv_watch) + alloc_log:check({ + -- Does not free anything. + }) + local lws = { list_watch(l), list_watch(l), list_watch(l) } + alloc_log:clear() + lib.tv_list_watch_remove(l, lws[2]) + eq(lws[3], l.lv_watch) + eq(lws[1], l.lv_watch.lw_next) + lib.tv_list_watch_remove(l, lws[1]) + eq(lws[3], l.lv_watch) + eq(nil, l.lv_watch.lw_next) + lib.tv_list_watch_remove(l, lws[3]) + eq(nil, l.lv_watch) + alloc_log:check({ + -- Does not free anything. + }) + end) + itp('ignores not found watchers', function() + local l = list(1, 2, 3, 4, 5, 6, 7) + local lw = list_watch_alloc() + lib.tv_list_watch_remove(l, lw) + end) + end) + end) + -- add() and fix() were tested when testing tv_list_item_remove() + describe('free()', function() + itp('recursively frees list', function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free(l1) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(4), + alloc_rets:freed(1), + }) + lib.tv_list_free(l2) + alloc_log:check({ + alloc_rets:freed(6), + alloc_rets:freed(7), + alloc_rets:freed(5), + }) + lib.tv_list_free(l3) + alloc_log:check({ + alloc_rets:freed(9), + alloc_rets:freed(10), + alloc_rets:freed(8), + }) + end) + end) + describe('free_list()', function() + itp('does not free list contents', function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free_list(l1) + alloc_log:check({ + alloc_rets:freed(1), + }) + lib.tv_list_free_list(l2) + alloc_log:check({ + alloc_rets:freed(5), + }) + lib.tv_list_free_list(l3) + alloc_log:check({ + alloc_rets:freed(8), + }) + end) + end) + describe('free_contents()', function() + itp('recursively frees list, except for the list structure itself', + function() + local l1 = ffi.gc(list(1, 'abc'), nil) + local l2 = ffi.gc(list({}), nil) + local l3 = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l1), + a.li(l1.lv_first), + a.str(l1.lv_last.li_tv.vval.v_string, #('abc')), + a.li(l1.lv_last), + a.list(l2), + a.dict(l2.lv_first.li_tv.vval.v_dict), + a.li(l2.lv_first), + a.list(l3), + a.list(l3.lv_first.li_tv.vval.v_list), + a.li(l3.lv_first), + }, alloc_rets)) + lib.tv_list_free_contents(l1) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(4), + }) + lib.tv_list_free_contents(l2) + alloc_log:check({ + alloc_rets:freed(6), + alloc_rets:freed(7), + }) + lib.tv_list_free_contents(l3) + alloc_log:check({ + alloc_rets:freed(9), + alloc_rets:freed(10), + }) + end) + end) + describe('unref()', function() + itp('recursively frees list when reference count goes to 0', function() + local l = ffi.gc(list(empty_list), nil) + local alloc_rets = {} + alloc_log:check(get_alloc_rets({ + a.list(l), + a.list(l.lv_first.li_tv.vval.v_list), + a.li(l.lv_first), + }, alloc_rets)) + l.lv_refcount = 2 + lib.tv_list_unref(l) + alloc_log:check({}) + lib.tv_list_unref(l) + alloc_log:check({ + alloc_rets:freed(2), + alloc_rets:freed(3), + alloc_rets:freed(1), + }) + end) + end) + describe('remove_items()', function() + itp('works', function() + local l_tv = lua2typvalt({1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}) + local l = l_tv.vval.v_list + local lis = list_items(l) + -- Three watchers: pointing to first, middle and last elements. + local lws = { + list_watch(l, lis[1]), + list_watch(l, lis[7]), + list_watch(l, lis[13]), + } + alloc_log:clear() + + lib.tv_list_remove_items(l, lis[1], lis[3]) + eq({4, 5, 6, 7, 8, 9, 10, 11, 12, 13}, typvalt2lua(l_tv)) + eq({lis[4], lis[7], lis[13]}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item}) + + lib.tv_list_remove_items(l, lis[11], lis[13]) + eq({4, 5, 6, 7, 8, 9, 10}, typvalt2lua(l_tv)) + eq({lis[4], lis[7], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_remove_items(l, lis[6], lis[8]) + eq({4, 5, 9, 10}, typvalt2lua(l_tv)) + eq({lis[4], lis[9], nil}, {lws[1].lw_item, lws[2].lw_item, lws[3].lw_item == nil and nil}) + + lib.tv_list_remove_items(l, lis[4], lis[10]) + eq(empty_list, typvalt2lua(l_tv)) + eq({true, true, true}, {lws[1].lw_item == nil, lws[2].lw_item == nil, lws[3].lw_item == nil}) + + alloc_log:check({}) + end) + end) + describe('insert', function() + describe('()', function() + itp('works', function() + local l_tv = lua2typvalt({1, 2, 3, 4, 5, 6, 7}) + local l = l_tv.vval.v_list + local lis = list_items(l) + local li + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=100500}} + lib.tv_list_insert(l, li, nil) + eq(l.lv_last, li) + eq({1, 2, 3, 4, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=0}} + lib.tv_list_insert(l, li, lis[1]) + eq(l.lv_first, li) + eq({0, 1, 2, 3, 4, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + + li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=4.5}} + lib.tv_list_insert(l, li, lis[5]) + eq(list_items(l)[6], li) + eq({0, 1, 2, 3, 4, 4.5, 5, 6, 7, 100500}, typvalt2lua(l_tv)) + end) + itp('works with an empty list', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + eq(nil, l.lv_first) + eq(nil, l.lv_last) + + local li = li_alloc(true) + li.li_tv = {v_type=lib.VAR_FLOAT, vval={v_float=100500}} + lib.tv_list_insert(l, li, nil) + eq(l.lv_last, li) + eq({100500}, typvalt2lua(l_tv)) + end) + end) + describe('tv()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_l_tv = lua2typvalt(empty_list) + alloc_log:clear() + local l_l = l_l_tv.vval.v_list + eq(1, l_l.lv_refcount) + lib.tv_list_insert_tv(l, l_l_tv, nil) + eq(2, l_l.lv_refcount) + eq(l_l, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_first), + }) + + local l_s_tv = lua2typvalt('test') + alloc_log:check({ + a.str(l_s_tv.vval.v_string, 'test'), + }) + lib.tv_list_insert_tv(l, l_s_tv, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.str(l.lv_first.li_tv.vval.v_string, 'test'), + }) + + eq({'test', empty_list}, typvalt2lua(l_tv)) + end) + end) + end) + describe('append', function() + describe('list()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_l = list(1) + alloc_log:clear() + eq(1, l_l.lv_refcount) + lib.tv_list_append_list(l, l_l) + eq(2, l_l.lv_refcount) + eq(l_l, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_list(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({{1}, null_list}, typvalt2lua(l_tv)) + end) + end) + describe('dict()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local l_d_tv = lua2typvalt({test=1}) + local l_d = l_d_tv.vval.v_dict + alloc_log:clear() + eq(1, l_d.dv_refcount) + lib.tv_list_append_dict(l, l_d) + eq(2, l_d.dv_refcount) + eq(l_d, l.lv_first.li_tv.vval.v_list) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_dict(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({{test=1}, null_dict}, typvalt2lua(l_tv)) + end) + end) + describe('string()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + alloc_log:clear() + lib.tv_list_append_string(l, 'test', 3) + alloc_log:check({ + a.str(l.lv_last.li_tv.vval.v_string, 'tes'), + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, nil, 0) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, nil, -1) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_string(l, 'test', -1) + alloc_log:check({ + a.str(l.lv_last.li_tv.vval.v_string, 'test'), + a.li(l.lv_last), + }) + + eq({'tes', null_string, null_string, 'test'}, typvalt2lua(l_tv)) + end) + end) + describe('allocated string()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + local s = lib.xstrdup('test') + alloc_log:clear() + lib.tv_list_append_allocated_string(l, s) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_allocated_string(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_allocated_string(l, nil) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({'test', null_string, null_string}, typvalt2lua(l_tv)) + end) + end) + describe('number()', function() + itp('works', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + + alloc_log:clear() + lib.tv_list_append_number(l, -100500) + alloc_log:check({ + a.li(l.lv_last), + }) + + lib.tv_list_append_number(l, 100500) + alloc_log:check({ + a.li(l.lv_last), + }) + + eq({int(-100500), int(100500)}, typvalt2lua(l_tv)) + end) + end) + end) + describe('copy()', function() + local function tv_list_copy(...) + return ffi.gc(lib.tv_list_copy(...), lib.tv_list_unref) + end + itp('copies NULL correctly', function() + eq(nil, lib.tv_list_copy(nil, nil, true, 0)) + eq(nil, lib.tv_list_copy(nil, nil, false, 0)) + eq(nil, lib.tv_list_copy(nil, nil, true, 1)) + eq(nil, lib.tv_list_copy(nil, nil, false, 1)) + end) + itp('copies list correctly without converting items', function() + do + local v = {{['«']='»'}, {'„'}, 1, '“', null_string, null_list, null_dict} + local l_tv = lua2typvalt(v) + local l = l_tv.vval.v_list + local lis = list_items(l) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_copy1 = tv_list_copy(nil, l, false, 0) + eq(2, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(2, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_copy1 = list_items(l_copy1) + eq(lis[1].li_tv.vval.v_dict, lis_copy1[1].li_tv.vval.v_dict) + eq(lis[2].li_tv.vval.v_list, lis_copy1[2].li_tv.vval.v_list) + eq(v, lst2tbl(l_copy1)) + alloc_log:check({ + a.list(l_copy1), + a.li(lis_copy1[1]), + a.li(lis_copy1[2]), + a.li(lis_copy1[3]), + a.li(lis_copy1[4]), + a.str(lis_copy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_copy1[5]), + a.li(lis_copy1[6]), + a.li(lis_copy1[7]), + }) + lib.tv_list_free(ffi.gc(l_copy1, nil)) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_deepcopy1 = tv_list_copy(nil, l, true, 0) + neq(nil, l_deepcopy1) + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_deepcopy1 = list_items(l_deepcopy1) + neq(lis[1].li_tv.vval.v_dict, lis_deepcopy1[1].li_tv.vval.v_dict) + neq(lis[2].li_tv.vval.v_list, lis_deepcopy1[2].li_tv.vval.v_list) + eq(v, lst2tbl(l_deepcopy1)) + local di_deepcopy1 = first_di(lis_deepcopy1[1].li_tv.vval.v_dict) + alloc_log:check({ + a.list(l_deepcopy1), + a.li(lis_deepcopy1[1]), + a.dict(lis_deepcopy1[1].li_tv.vval.v_dict), + a.di(di_deepcopy1, #('«')), + a.str(di_deepcopy1.di_tv.vval.v_string, #v[1]['«']), + a.li(lis_deepcopy1[2]), + a.list(lis_deepcopy1[2].li_tv.vval.v_list), + a.li(lis_deepcopy1[2].li_tv.vval.v_list.lv_first), + a.str(lis_deepcopy1[2].li_tv.vval.v_list.lv_first.li_tv.vval.v_string, #v[2][1]), + a.li(lis_deepcopy1[3]), + a.li(lis_deepcopy1[4]), + a.str(lis_deepcopy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_deepcopy1[5]), + a.li(lis_deepcopy1[6]), + a.li(lis_deepcopy1[7]), + }) + end + collectgarbage() + end) + itp('copies list correctly and converts items', function() + local vc = ffi.gc(ffi.new('vimconv_T[1]'), function(vc) + lib.convert_setup(vc, nil, nil) + end) + -- UTF-8 ↔ latin1 conversions need no iconv + eq(OK, lib.convert_setup(vc, to_cstr('utf-8'), to_cstr('latin1'))) + + local v = {{['«']='»'}, {'„'}, 1, '“', null_string, null_list, null_dict} + local l_tv = lua2typvalt(v) + local l = l_tv.vval.v_list + local lis = list_items(l) + alloc_log:clear() + + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local l_deepcopy1 = tv_list_copy(vc, l, true, 0) + neq(nil, l_deepcopy1) + eq(1, lis[1].li_tv.vval.v_dict.dv_refcount) + eq(1, lis[2].li_tv.vval.v_list.lv_refcount) + local lis_deepcopy1 = list_items(l_deepcopy1) + neq(lis[1].li_tv.vval.v_dict, lis_deepcopy1[1].li_tv.vval.v_dict) + neq(lis[2].li_tv.vval.v_list, lis_deepcopy1[2].li_tv.vval.v_list) + eq({{['\171']='\187'}, {'\191'}, 1, '\191', null_string, null_list, null_dict}, + lst2tbl(l_deepcopy1)) + local di_deepcopy1 = first_di(lis_deepcopy1[1].li_tv.vval.v_dict) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.list(l_deepcopy1), + a.li(lis_deepcopy1[1]), + a.dict(lis_deepcopy1[1].li_tv.vval.v_dict), + a.di(di_deepcopy1, 1), + a.str(di_deepcopy1.di_tv.vval.v_string, 2), + a.li(lis_deepcopy1[2]), + a.list(lis_deepcopy1[2].li_tv.vval.v_list), + a.li(lis_deepcopy1[2].li_tv.vval.v_list.lv_first), + a.str(lis_deepcopy1[2].li_tv.vval.v_list.lv_first.li_tv.vval.v_string, #v[2][1]), + a.li(lis_deepcopy1[3]), + a.li(lis_deepcopy1[4]), + a.str(lis_deepcopy1[4].li_tv.vval.v_string, #v[4]), + a.li(lis_deepcopy1[5]), + a.li(lis_deepcopy1[6]), + a.li(lis_deepcopy1[7]), + }) + end) + itp('returns different/same containers with(out) copyID', function() + local l_inner_tv = lua2typvalt(empty_list) + local l_tv = lua2typvalt({l_inner_tv, l_inner_tv}) + eq(3, l_inner_tv.vval.v_list.lv_refcount) + local l = l_tv.vval.v_list + eq(l.lv_first.li_tv.vval.v_list, l.lv_last.li_tv.vval.v_list) + + local l_copy1 = tv_list_copy(nil, l, true, 0) + neq(l_copy1.lv_first.li_tv.vval.v_list, l_copy1.lv_last.li_tv.vval.v_list) + eq({empty_list, empty_list}, lst2tbl(l_copy1)) + + local l_copy2 = tv_list_copy(nil, l, true, 2) + eq(l_copy2.lv_first.li_tv.vval.v_list, l_copy2.lv_last.li_tv.vval.v_list) + eq({empty_list, empty_list}, lst2tbl(l_copy2)) + + eq(3, l_inner_tv.vval.v_list.lv_refcount) + end) + itp('works with self-referencing list with copyID', function() + local l_tv = lua2typvalt(empty_list) + local l = l_tv.vval.v_list + eq(1, l.lv_refcount) + lib.tv_list_append_list(l, l) + eq(2, l.lv_refcount) + + local l_copy1 = tv_list_copy(nil, l, true, 2) + eq(2, l_copy1.lv_refcount) + local v = {} + v[1] = v + eq(v, lst2tbl(l_copy1)) + + local lis = list_items(l) + lib.tv_list_item_remove(l, lis[1]) + eq(1, l.lv_refcount) + + local lis_copy1 = list_items(l_copy1) + lib.tv_list_item_remove(l_copy1, lis_copy1[1]) + eq(1, l_copy1.lv_refcount) + end) + end) + describe('extend()', function() + itp('can extend list with itself', function() + local l + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, nil) + alloc_log:check({ + a.li(l.lv_last.li_prev), + a.li(l.lv_last), + }) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq({1, {}, 1, {}}, lst2tbl(l)) + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, l.lv_last) + alloc_log:check({ + a.li(l.lv_last.li_prev.li_prev), + a.li(l.lv_last.li_prev), + }) + eq({1, 1, {}, {}}, lst2tbl(l)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.li(l.lv_first.li_next), + }) + eq({1, {}, 1, {}}, lst2tbl(l)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + end) + itp('can extend list with an empty list', function() + local l = list(1, {}) + local el = list() + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + + lib.tv_list_extend(l, el, nil) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + + lib.tv_list_extend(l, el, l.lv_first) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + + lib.tv_list_extend(l, el, l.lv_last) + alloc_log:check({ + }) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, el.lv_refcount) + eq({1, {}}, lst2tbl(l)) + end) + itp('can extend list with another non-empty list', function() + local l + local l2 = list(42, empty_list) + eq(1, l2.lv_refcount) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, nil) + alloc_log:check({ + a.li(l.lv_last.li_prev), + a.li(l.lv_last), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({1, {}, 42, empty_list}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, l.lv_first) + alloc_log:check({ + a.li(l.lv_first), + a.li(l.lv_first.li_next), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({42, empty_list, 1, {}}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + + l = ffi.gc(list(1, {}), nil) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + lib.tv_list_extend(l, l2, l.lv_last) + alloc_log:check({ + a.li(l.lv_first.li_next), + a.li(l.lv_first.li_next.li_next), + }) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + eq({1, 42, empty_list, {}}, lst2tbl(l)) + lib.tv_list_free(l) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + end) + end) + describe('concat()', function() + itp('works with NULL lists', function() + local l = list(1, {}) + alloc_log:clear() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv1 = typvalt() + eq(OK, lib.tv_list_concat(nil, l, rettv1)) + eq(1, l.lv_refcount) + eq(tonumber(lib.VAR_LIST), tonumber(rettv1.v_type)) + eq({1, {}}, typvalt2lua(rettv1)) + eq(1, rettv1.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv1.vval.v_list), + a.li(rettv1.vval.v_list.lv_first), + a.li(rettv1.vval.v_list.lv_last), + }) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv2 = typvalt() + eq(OK, lib.tv_list_concat(l, nil, rettv2)) + eq(1, l.lv_refcount) + eq(tonumber(lib.VAR_LIST), tonumber(rettv2.v_type)) + eq({1, {}}, typvalt2lua(rettv2)) + eq(1, rettv2.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv2.vval.v_list), + a.li(rettv2.vval.v_list.lv_first), + a.li(rettv2.vval.v_list.lv_last), + }) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + + local rettv3 = typvalt() + eq(OK, lib.tv_list_concat(nil, nil, rettv3)) + eq(tonumber(lib.VAR_LIST), tonumber(rettv3.v_type)) + eq(null_list, typvalt2lua(rettv3)) + alloc_log:check({}) + end) + itp('works with two different lists', function() + local l1 = list(1, {}) + local l2 = list(3, empty_list) + eq(1, l1.lv_refcount) + eq(1, l1.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, l2.lv_refcount) + eq(1, l2.lv_last.li_tv.vval.v_list.lv_refcount) + alloc_log:clear() + + local rettv = typvalt() + eq(OK, lib.tv_list_concat(l1, l2, rettv)) + eq(1, l1.lv_refcount) + eq(2, l1.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, l2.lv_refcount) + eq(2, l2.lv_last.li_tv.vval.v_list.lv_refcount) + alloc_log:check({ + a.list(rettv.vval.v_list), + a.li(rettv.vval.v_list.lv_first), + a.li(rettv.vval.v_list.lv_first.li_next), + a.li(rettv.vval.v_list.lv_last.li_prev), + a.li(rettv.vval.v_list.lv_last), + }) + eq({1, {}, 3, empty_list}, typvalt2lua(rettv)) + end) + itp('can concatenate list with itself', function() + local l = list(1, {}) + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + alloc_log:clear() + + local rettv = typvalt() + eq(OK, lib.tv_list_concat(l, l, rettv)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + alloc_log:check({ + a.list(rettv.vval.v_list), + a.li(rettv.vval.v_list.lv_first), + a.li(rettv.vval.v_list.lv_first.li_next), + a.li(rettv.vval.v_list.lv_last.li_prev), + a.li(rettv.vval.v_list.lv_last), + }) + eq({1, {}, 1, {}}, typvalt2lua(rettv)) + end) + itp('can concatenate empty non-NULL lists', function() + local l = list(1, {}) + local le = list() + local le2 = list() + eq(1, l.lv_refcount) + eq(1, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:clear() + + local rettv1 = typvalt() + eq(OK, lib.tv_list_concat(l, le, rettv1)) + eq(1, l.lv_refcount) + eq(2, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv1.vval.v_list), + a.li(rettv1.vval.v_list.lv_first), + a.li(rettv1.vval.v_list.lv_last), + }) + eq({1, {}}, typvalt2lua(rettv1)) + + local rettv2 = typvalt() + eq(OK, lib.tv_list_concat(le, l, rettv2)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv2.vval.v_list), + a.li(rettv2.vval.v_list.lv_first), + a.li(rettv2.vval.v_list.lv_last), + }) + eq({1, {}}, typvalt2lua(rettv2)) + + local rettv3 = typvalt() + eq(OK, lib.tv_list_concat(le, le, rettv3)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv3.vval.v_list), + }) + eq(empty_list, typvalt2lua(rettv3)) + + local rettv4 = typvalt() + eq(OK, lib.tv_list_concat(le, le2, rettv4)) + eq(1, l.lv_refcount) + eq(3, l.lv_last.li_tv.vval.v_dict.dv_refcount) + eq(1, le.lv_refcount) + eq(1, le2.lv_refcount) + alloc_log:check({ + a.list(rettv4.vval.v_list), + }) + eq(empty_list, typvalt2lua(rettv4)) + end) + end) + describe('join()', function() + local function list_join(l, sep, ret) + local ga = ga_alloc() + eq(ret or OK, lib.tv_list_join(ga, l, sep)) + if ga.ga_data == nil then return '' + else return ffi.string(ga.ga_data) + end + end + itp('works', function() + local l + l = list('boo', 'far') + eq('boo far', list_join(l, ' ')) + eq('boofar', list_join(l, '')) + + l = list('boo') + eq('boo', list_join(l, ' ')) + + l = list() + eq('', list_join(l, ' ')) + + l = list({}, 'far') + eq('{} far', list_join(l, ' ')) + + local recursive_list = {} + recursive_list[1] = recursive_list + l = ffi.gc(list(recursive_list, 'far'), nil) + eq('[[...@0]] far', list_join(l, ' ')) + + local recursive_l = l.lv_first.li_tv.vval.v_list + local recursive_li = recursive_l.lv_first + lib.tv_list_item_remove(recursive_l, recursive_li) + lib.tv_list_free(l) + end) + end) + describe('equal()', function() + itp('compares empty and NULL lists correctly', function() + local l = list() + local l2 = list() + + -- NULL lists are not equal to empty lists + eq(false, lib.tv_list_equal(l, nil, true, false)) + eq(false, lib.tv_list_equal(nil, l, false, false)) + eq(false, lib.tv_list_equal(nil, l, false, true)) + eq(false, lib.tv_list_equal(l, nil, true, true)) + + -- Yet NULL lists are equal themselves + eq(true, lib.tv_list_equal(nil, nil, true, false)) + eq(true, lib.tv_list_equal(nil, nil, false, false)) + eq(true, lib.tv_list_equal(nil, nil, false, true)) + eq(true, lib.tv_list_equal(nil, nil, true, true)) + + -- As well as empty lists + eq(true, lib.tv_list_equal(l, l, true, false)) + eq(true, lib.tv_list_equal(l, l2, false, false)) + eq(true, lib.tv_list_equal(l2, l, false, true)) + eq(true, lib.tv_list_equal(l2, l2, true, true)) + end) + -- Must not use recursive=true argument in the following tests because it + -- indicates that tv_equal_recurse_limit and recursive_cnt were set which + -- is essential. This argument will be set when comparing inner lists. + itp('compares lists correctly when case is not ignored', function() + local l1 = list('abc', {1, 2, 'Abc'}, 'def') + local l2 = list('abc', {1, 2, 'Abc'}) + local l3 = list('abc', {1, 2, 'Abc'}, 'Def') + local l4 = list('abc', {1, 2, 'Abc', 4}, 'def') + local l5 = list('Abc', {1, 2, 'Abc'}, 'def') + local l6 = list('abc', {1, 2, 'Abc'}, 'def') + local l7 = list('abc', {1, 2, 'abc'}, 'def') + local l8 = list('abc', nil, 'def') + local l9 = list('abc', {1, 2, nil}, 'def') + + eq(true, lib.tv_list_equal(l1, l1, false, false)) + eq(false, lib.tv_list_equal(l1, l2, false, false)) + eq(false, lib.tv_list_equal(l1, l3, false, false)) + eq(false, lib.tv_list_equal(l1, l4, false, false)) + eq(false, lib.tv_list_equal(l1, l5, false, false)) + eq(true, lib.tv_list_equal(l1, l6, false, false)) + eq(false, lib.tv_list_equal(l1, l7, false, false)) + eq(false, lib.tv_list_equal(l1, l8, false, false)) + eq(false, lib.tv_list_equal(l1, l9, false, false)) + end) + itp('compares lists correctly when case is ignored', function() + local l1 = list('abc', {1, 2, 'Abc'}, 'def') + local l2 = list('abc', {1, 2, 'Abc'}) + local l3 = list('abc', {1, 2, 'Abc'}, 'Def') + local l4 = list('abc', {1, 2, 'Abc', 4}, 'def') + local l5 = list('Abc', {1, 2, 'Abc'}, 'def') + local l6 = list('abc', {1, 2, 'Abc'}, 'def') + local l7 = list('abc', {1, 2, 'abc'}, 'def') + local l8 = list('abc', nil, 'def') + local l9 = list('abc', {1, 2, nil}, 'def') + + eq(true, lib.tv_list_equal(l1, l1, true, false)) + eq(false, lib.tv_list_equal(l1, l2, true, false)) + eq(true, lib.tv_list_equal(l1, l3, true, false)) + eq(false, lib.tv_list_equal(l1, l4, true, false)) + eq(true, lib.tv_list_equal(l1, l5, true, false)) + eq(true, lib.tv_list_equal(l1, l6, true, false)) + eq(true, lib.tv_list_equal(l1, l7, true, false)) + eq(false, lib.tv_list_equal(l1, l8, true, false)) + eq(false, lib.tv_list_equal(l1, l9, true, false)) + end) + end) + describe('find', function() + describe('()', function() + itp('correctly indexes list', function() + local l = list(1, 2, 3, 4, 5) + local lis = list_items(l) + alloc_log:clear() + + eq(nil, lib.tv_list_find(nil, -1)) + eq(nil, lib.tv_list_find(nil, 0)) + eq(nil, lib.tv_list_find(nil, 1)) + + eq(nil, lib.tv_list_find(l, 5)) + eq(nil, lib.tv_list_find(l, -6)) + eq(lis[1], lib.tv_list_find(l, -5)) + eq(lis[5], lib.tv_list_find(l, 4)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + + l.lv_idx_item = nil + eq(lis[1], lib.tv_list_find(l, -5)) + l.lv_idx_item = nil + eq(lis[5], lib.tv_list_find(l, 4)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, -3)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, -3)) + + l.lv_idx_item = nil + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[1], lib.tv_list_find(l, -5)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[5], lib.tv_list_find(l, 4)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, 2)) + eq(lis[3], lib.tv_list_find(l, -3)) + + alloc_log:check({}) + end) + end) + describe('nr()', function() + local function tv_list_find_nr(l, n, msg) + return check_emsg(function() + local err = ffi.new('bool[1]', {false}) + local ret = lib.tv_list_find_nr(l, n, err) + return (err[0] == true), ret + end, msg) + end + itp('returns correct number', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq({false, 1}, {tv_list_find_nr(l, -5)}) + eq({false, 5}, {tv_list_find_nr(l, 4)}) + eq({false, 3}, {tv_list_find_nr(l, 2)}) + eq({false, 3}, {tv_list_find_nr(l, -3)}) + + alloc_log:check({}) + end) + itp('returns correct number when given a string', function() + local l = list('1', '2', '3', '4', '5') + alloc_log:clear() + + eq({false, 1}, {tv_list_find_nr(l, -5)}) + eq({false, 5}, {tv_list_find_nr(l, 4)}) + eq({false, 3}, {tv_list_find_nr(l, 2)}) + eq({false, 3}, {tv_list_find_nr(l, -3)}) + + alloc_log:check({}) + end) + itp('returns zero when given a NULL string', function() + local l = list(null_string) + alloc_log:clear() + + eq({false, 0}, {tv_list_find_nr(l, 0)}) + + alloc_log:check({}) + end) + itp('errors out on NULL lists', function() + eq({true, -1}, {tv_list_find_nr(nil, -5)}) + eq({true, -1}, {tv_list_find_nr(nil, 4)}) + eq({true, -1}, {tv_list_find_nr(nil, 2)}) + eq({true, -1}, {tv_list_find_nr(nil, -3)}) + + alloc_log:check({}) + end) + itp('errors out on out-of-range indexes', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq({true, -1}, {tv_list_find_nr(l, -6)}) + eq({true, -1}, {tv_list_find_nr(l, 5)}) + + alloc_log:check({}) + end) + itp('errors out on invalid types', function() + local l = list(1, empty_list, {}) + + eq({true, 0}, {tv_list_find_nr(l, 0, 'E805: Using a Float as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, 1, 'E745: Using a List as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, 2, 'E728: Using a Dictionary as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -1, 'E728: Using a Dictionary as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -2, 'E745: Using a List as a Number')}) + eq({true, 0}, {tv_list_find_nr(l, -3, 'E805: Using a Float as a Number')}) + end) + end) + local function tv_list_find_str(l, n, msg) + return check_emsg(function() + local ret = lib.tv_list_find_str(l, n) + local s = nil + if ret ~= nil then + s = ffi.string(ret) + end + return s + end, msg) + end + describe('str()', function() + itp('returns correct string', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + alloc_log:clear() + + eq('1', tv_list_find_str(l, -5)) + eq('5', tv_list_find_str(l, 4)) + eq('3', tv_list_find_str(l, 2)) + eq('3', tv_list_find_str(l, -3)) + + alloc_log:check({}) + end) + itp('returns string when used with VAR_STRING items', function() + local l = list('1', '2', '3', '4', '5') + alloc_log:clear() + + eq('1', tv_list_find_str(l, -5)) + eq('5', tv_list_find_str(l, 4)) + eq('3', tv_list_find_str(l, 2)) + eq('3', tv_list_find_str(l, -3)) + + alloc_log:check({}) + end) + itp('returns empty when used with NULL string', function() + local l = list(null_string) + alloc_log:clear() + + eq('', tv_list_find_str(l, 0)) + + alloc_log:check({}) + end) + itp('fails with error message when index is out of range', function() + local l = list(int(1), int(2), int(3), int(4), int(5)) + + eq(nil, tv_list_find_str(l, -6, 'E684: list index out of range: -6')) + eq(nil, tv_list_find_str(l, 5, 'E684: list index out of range: 5')) + end) + itp('fails with error message on invalid types', function() + local l = list(1, empty_list, {}) + + eq('', tv_list_find_str(l, 0, 'E806: using Float as a String')) + eq('', tv_list_find_str(l, 1, 'E730: using List as a String')) + eq('', tv_list_find_str(l, 2, 'E731: using Dictionary as a String')) + eq('', tv_list_find_str(l, -1, 'E731: using Dictionary as a String')) + eq('', tv_list_find_str(l, -2, 'E730: using List as a String')) + eq('', tv_list_find_str(l, -3, 'E806: using Float as a String')) + end) + end) + end) + describe('idx_of_item()', function() + itp('works', function() + local l = list(1, 2, 3, 4, 5) + local l2 = list(42, empty_list) + local lis = list_items(l) + local lis2 = list_items(l2) + + for i, li in ipairs(lis) do + eq(i - 1, lib.tv_list_idx_of_item(l, li)) + end + eq(-1, lib.tv_list_idx_of_item(l, lis2[1])) + eq(-1, lib.tv_list_idx_of_item(l, nil)) + eq(-1, lib.tv_list_idx_of_item(nil, nil)) + eq(-1, lib.tv_list_idx_of_item(nil, lis[1])) + end) + end) + end) + describe('dict', function() + describe('watcher', function() + describe('add()/remove()', function() + itp('works with an empty key', function() + local d = dict({}) + eq({}, dict_watchers(d)) + local cb = ffi.gc(tbl2callback({type='none'}), nil) + alloc_log:clear() + lib.tv_dict_watcher_add(d, '*', 0, cb[0]) + local ws, qs = dict_watchers(d) + local key_p = qs[1].key_pattern + alloc_log:check({ + a.dwatcher(qs[1]), + a.str(key_p, 0), + }) + eq({{busy=false, cb={type='none'}, pat=''}}, ws) + eq(true, lib.tv_dict_watcher_remove(d, 'x', 0, cb[0])) + alloc_log:check({ + a.freed(key_p), + a.freed(qs[1]), + }) + eq({}, dict_watchers(d)) + end) + itp('works with multiple callbacks', function() + local d = dict({}) + eq({}, dict_watchers(d)) + alloc_log:check({a.dict(d)}) + local cbs = {} + cbs[1] = {'te', ffi.gc(tbl2callback({type='none'}), nil)} + alloc_log:check({}) + cbs[2] = {'foo', ffi.gc(tbl2callback({type='fref', fref='tr'}), nil)} + alloc_log:check({ + a.str(cbs[2][2].data.funcref, #('tr')), + }) + cbs[3] = {'te', ffi.gc(tbl2callback({type='pt', fref='tr', pt={ + value='tr', + args={'test'}, + dict={}, + }}), nil)} + local pt3 = cbs[3][2].data.partial + local pt3_argv = pt3.pt_argv + local pt3_dict = pt3.pt_dict + local pt3_name = pt3.pt_name + local pt3_str_arg = pt3.pt_argv[0].vval.v_string + alloc_log:check({ + a.lua_pt(pt3), + a.lua_tvs(pt3_argv, pt3.pt_argc), + a.str(pt3_str_arg, #('test')), + a.dict(pt3_dict), + a.str(pt3_name, #('tr')), + }) + for _, v in ipairs(cbs) do + lib.tv_dict_watcher_add(d, v[1], #(v[1]), v[2][0]) + end + local ws, qs, kps = dict_watchers(d) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}, + {busy=false, pat=cbs[2][1], cb={type='fref', fref='tr'}}, + {busy=false, pat=cbs[3][1], cb={type='pt', fref='tr', pt={ + [type_key]=func_type, + value='tr', + args={'test'}, + dict={}, + }}}}, ws) + alloc_log:check({ + a.dwatcher(qs[1]), + a.str(kps[1][1], kps[1][2]), + a.dwatcher(qs[2]), + a.str(kps[2][1], kps[2][2]), + a.dwatcher(qs[3]), + a.str(kps[3][1], kps[3][2]), + }) + eq(true, lib.tv_dict_watcher_remove(d, cbs[2][1], #cbs[2][1], cbs[2][2][0])) + alloc_log:check({ + a.freed(cbs[2][2].data.funcref), + a.freed(kps[2][1]), + a.freed(qs[2]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[2][1], #cbs[2][1], cbs[2][2][0])) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}, + {busy=false, pat=cbs[3][1], cb={type='pt', fref='tr', pt={ + [type_key]=func_type, + value='tr', + args={'test'}, + dict={}, + }}}}, dict_watchers(d)) + eq(true, lib.tv_dict_watcher_remove(d, cbs[3][1], #cbs[3][1], cbs[3][2][0])) + alloc_log:check({ + a.freed(pt3_str_arg), + a.freed(pt3_argv), + a.freed(pt3_dict), + a.freed(pt3_name), + a.freed(pt3), + a.freed(kps[3][1]), + a.freed(qs[3]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[3][1], #cbs[3][1], cbs[3][2][0])) + eq({{busy=false, pat=cbs[1][1], cb={type='none'}}}, dict_watchers(d)) + eq(true, lib.tv_dict_watcher_remove(d, cbs[1][1], #cbs[1][1], cbs[1][2][0])) + alloc_log:check({ + a.freed(kps[1][1]), + a.freed(qs[1]), + }) + eq(false, lib.tv_dict_watcher_remove(d, cbs[1][1], #cbs[1][1], cbs[1][2][0])) + eq({}, dict_watchers(d)) + end) + end) + describe('notify', function() + -- Way too hard to test it here, functional tests in + -- dict_notifications_spec.lua. + end) + end) + describe('item', function() + describe('alloc()/free()', function() + local function check_tv_dict_item_alloc_len(s, len, tv, more_frees) + local di + if len == nil then + di = ffi.gc(lib.tv_dict_item_alloc(s), nil) + len = #s + else + di = ffi.gc(lib.tv_dict_item_alloc_len(s, len or #s), nil) + end + eq(s:sub(1, len), ffi.string(di.di_key)) + alloc_log:check({a.di(di, len)}) + if tv then + di.di_tv = tv + else + di.di_tv.v_type = lib.VAR_UNKNOWN + end + lib.tv_dict_item_free(di) + alloc_log:check(concat_tables(more_frees, {a.freed(di)})) + end + local function check_tv_dict_item_alloc(s, tv, more_frees) + return check_tv_dict_item_alloc_len(s, nil, tv, more_frees) + end + itp('works', function() + check_tv_dict_item_alloc('') + check_tv_dict_item_alloc('t') + check_tv_dict_item_alloc('TEST') + check_tv_dict_item_alloc_len('', 0) + check_tv_dict_item_alloc_len('TEST', 2) + local tv = lua2typvalt('test') + alloc_log:check({a.str(tv.vval.v_string, #('test'))}) + check_tv_dict_item_alloc('', tv, {a.freed(tv.vval.v_string)}) + tv = lua2typvalt('test') + alloc_log:check({a.str(tv.vval.v_string, #('test'))}) + check_tv_dict_item_alloc_len('', 0, tv, {a.freed(tv.vval.v_string)}) + end) + end) + describe('add()/remove()', function() + itp('works', function() + local d = dict() + eq({}, dct2tbl(d)) + alloc_log:check({a.dict(d)}) + local di = ffi.gc(lib.tv_dict_item_alloc(''), nil) + local tv = lua2typvalt('test') + di.di_tv = tv + alloc_log:check({a.di(di, ''), a.str(tv.vval.v_string, 'test')}) + eq(OK, lib.tv_dict_add(d, di)) + alloc_log:check({}) + eq(FAIL, check_emsg(function() return lib.tv_dict_add(d, di) end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.tv_dict_item_remove(d, di) + alloc_log:check({ + a.freed(tv.vval.v_string), + a.freed(di), + }) + end) + end) + end) + describe('indexing', function() + describe('find()', function() + local function tv_dict_find(d, key, key_len) + local di = lib.tv_dict_find(d, key, key_len or #key) + if di == nil then + return nil, nil, nil + end + return typvalt2lua(di.di_tv), ffi.string(di.di_key), di + end + itp('works with NULL dict', function() + eq(nil, lib.tv_dict_find(nil, '', 0)) + eq(nil, lib.tv_dict_find(nil, 'test', -1)) + eq(nil, lib.tv_dict_find(nil, nil, 0)) + end) + itp('works with NULL key', function() + local lua_d = { + ['']=0, + t=1, + te=2, + tes=3, + test=4, + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + local dis = dict_items(d) + eq({0, '', dis['']}, {tv_dict_find(d, '', 0)}) + eq({0, '', dis['']}, {tv_dict_find(d, nil, 0)}) + end) + itp('works with len properly', function() + local lua_d = { + ['']=0, + t=1, + te=2, + tes=3, + test=4, + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + for i = 0, 5 do + local v, k = tv_dict_find(d, 'testt', i) + eq({i, ('testt'):sub(1, i)}, {v, k}) + end + eq(nil, tv_dict_find(d, 'testt', 6)) -- Should take NUL byte + eq(5, tv_dict_find(d, 'testt', -1)) + alloc_log:check({}) + end) + end) + describe('get_number()', function() + itp('works with NULL dict', function() + eq(0, check_emsg(function() return lib.tv_dict_get_number(nil, 'test') end, + nil)) + end) + itp('works', function() + local d = ffi.gc(dict({test={}}), nil) + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 'test') end, + 'E728: Using a Dictionary as a Number')) + d = ffi.gc(dict({tes=int(42), t=44, te='43'}), nil) + alloc_log:clear() + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 'test') end, + nil)) + eq(42, check_emsg(function() return lib.tv_dict_get_number(d, 'tes') end, + nil)) + eq(43, check_emsg(function() return lib.tv_dict_get_number(d, 'te') end, + nil)) + alloc_log:check({}) + eq(0, check_emsg(function() return lib.tv_dict_get_number(d, 't') end, + 'E805: Using a Float as a Number')) + end) + end) + describe('get_string()', function() + itp('works with NULL dict', function() + eq(nil, check_emsg(function() return lib.tv_dict_get_string(nil, 'test', false) end, + nil)) + end) + itp('works', function() + local d = ffi.gc(dict({test={}}), nil) + eq('', ffi.string(check_emsg(function() return lib.tv_dict_get_string(d, 'test', false) end, + 'E731: using Dictionary as a String'))) + d = ffi.gc(dict({tes=int(42), t=44, te='43', xx=int(45)}), nil) + alloc_log:clear() + local dis = dict_items(d) + eq(nil, check_emsg(function() return lib.tv_dict_get_string(d, 'test', false) end, + nil)) + local s42 = check_emsg(function() return lib.tv_dict_get_string(d, 'tes', false) end, + nil) + eq('42', ffi.string(s42)) + local s45 = check_emsg(function() return lib.tv_dict_get_string(d, 'xx', false) end, + nil) + eq(s42, s45) + eq('45', ffi.string(s45)) + eq('45', ffi.string(s42)) + local s43 = check_emsg(function() return lib.tv_dict_get_string(d, 'te', false) end, + nil) + eq('43', ffi.string(s43)) + neq(s42, s43) + eq(s43, dis.te.di_tv.vval.v_string) + alloc_log:check({}) + eq('', ffi.string(check_emsg(function() return lib.tv_dict_get_string(d, 't', false) end, + 'E806: using Float as a String'))) + end) + itp('allocates a string copy when requested', function() + local function tv_dict_get_string_alloc(d, key, emsg) + alloc_log:clear() + local ret = check_emsg(function() return lib.tv_dict_get_string(d, key, true) end, + emsg) + local s_ret = (ret ~= nil) and ffi.string(ret) or nil + if not emsg then + if s_ret then + alloc_log:check({a.str(ret, s_ret)}) + else + alloc_log:check({}) + end + end + lib.xfree(ret) + return s_ret + end + local d = ffi.gc(dict({test={}}), nil) + eq('', tv_dict_get_string_alloc(d, 'test', 'E731: using Dictionary as a String')) + d = ffi.gc(dict({tes=int(42), t=44, te='43', xx=int(45)}), nil) + alloc_log:clear() + eq(nil, tv_dict_get_string_alloc(d, 'test')) + eq('42', tv_dict_get_string_alloc(d, 'tes')) + eq('45', tv_dict_get_string_alloc(d, 'xx')) + eq('43', tv_dict_get_string_alloc(d, 'te')) + eq('', tv_dict_get_string_alloc(d, 't', 'E806: using Float as a String')) + end) + end) + describe('get_string_buf()', function() + local function tv_dict_get_string_buf(d, key, buf, emsg) + buf = buf or ffi.gc(lib.xmalloc(lib.NUMBUFLEN), lib.xfree) + alloc_log:clear() + local ret = check_emsg(function() return lib.tv_dict_get_string_buf(d, key, buf) end, + emsg) + local s_ret = (ret ~= nil) and ffi.string(ret) or nil + if not emsg then + alloc_log:check({}) + end + return s_ret, ret, buf + end + itp('works with NULL dict', function() + eq(nil, tv_dict_get_string_buf(nil, 'test')) + end) + itp('works', function() + local lua_d = { + ['']={}, + t=1, + te=int(2), + tes=empty_list, + test='tset', + testt=5, + } + local d = dict(lua_d) + alloc_log:clear() + eq(lua_d, dct2tbl(d)) + alloc_log:check({}) + local s, r, b + s, r, b = tv_dict_get_string_buf(d, 'test') + neq(r, b) + eq('tset', s) + s, r, b = tv_dict_get_string_buf(d, 't', nil, 'E806: using Float as a String') + neq(r, b) + eq('', s) + s, r, b = tv_dict_get_string_buf(d, 'te') + eq(r, b) + eq('2', s) + end) + end) + describe('get_callback()', function() + local function tv_dict_get_callback(d, key, key_len, emsg) + key_len = key_len or #key + local cb = ffi.gc(ffi.cast('Callback*', lib.xmalloc(ffi.sizeof('Callback'))), lib.callback_free) + alloc_log:clear() + local ret = check_emsg(function() + return lib.tv_dict_get_callback(d, key, key_len, cb) + end, emsg) + local cb_lua = callback2tbl(cb[0]) + return cb_lua, ret + end + itp('works with NULL dict', function() + eq({{type='none'}, true}, {tv_dict_get_callback(nil, '')}) + end) + itp('works', function() + local lua_d = { + ['']='tr', + t=int(1), + te={[type_key]=func_type, value='tr'}, + tes={[type_key]=func_type, value='tr', args={'a', 'b'}}, + test={[type_key]=func_type, value='Test', dict={test=1}, args={}}, + testt={[type_key]=func_type, value='Test', dict={test=1}, args={1}}, + } + local d = dict(lua_d) + eq(lua_d, dct2tbl(d)) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, nil, 0)}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, '', -1)}) + eq({{type='none'}, true}, + {tv_dict_get_callback(d, 'x', -1)}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, 'testt', 0)}) + eq({{type='none'}, false}, + {tv_dict_get_callback(d, 'test', 1, 'E6000: Argument is not a function or function name')}) + eq({{type='fref', fref='tr'}, true}, + {tv_dict_get_callback(d, 'testt', 2)}) + eq({{ type='pt', fref='tr', pt={ [type_key]=func_type, value='tr', args={ 'a', 'b' } } }, true}, + {tv_dict_get_callback(d, 'testt', 3)}) + eq({{ type='pt', fref='Test', pt={ [type_key]=func_type, value='Test', dict={ test=1 }, args={} } }, true}, + {tv_dict_get_callback(d, 'testt', 4)}) + eq({{ type='pt', fref='Test', pt={ [type_key]=func_type, value='Test', dict={ test=1 }, args={1} } }, true}, + {tv_dict_get_callback(d, 'testt', 5)}) + end) + end) + end) + describe('add', function() + describe('()', function() + itp('works', function() + local di = lib.tv_dict_item_alloc_len('t-est', 5) + alloc_log:check({a.di(di, 't-est')}) + di.di_tv.v_type = lib.VAR_NUMBER + di.di_tv.vval.v_number = 42 + local d = dict({test=10}) + local dis = dict_items(d) + alloc_log:check({ + a.dict(d), + a.di(dis.test, 'test') + }) + eq({test=10}, dct2tbl(d)) + alloc_log:clear() + eq(OK, lib.tv_dict_add(d, di)) + alloc_log:check({}) + eq({test=10, ['t-est']=int(42)}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add(d, di) end, + 'E685: Internal error: hash_add()')) + end) + end) + describe('list()', function() + itp('works', function() + local l = list(1, 2, 3) + alloc_log:clear() + eq(1, l.lv_refcount) + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_list(d, 'testt', 3, l)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes={1, 2, 3}}, dct2tbl(d)) + eq(2, l.lv_refcount) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_list(d, 'testt', 3, l) end, + 'E685: Internal error: hash_add()')) + eq(2, l.lv_refcount) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_list(d, 'testt', 3, l) end, + nil)) + eq(2, l.lv_refcount) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('dict()', function() + itp('works', function() + local d2 = dict({foo=42}) + alloc_log:clear() + eq(1, d2.dv_refcount) + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_dict(d, 'testt', 3, d2)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes={foo=42}}, dct2tbl(d)) + eq(2, d2.dv_refcount) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_dict(d, 'testt', 3, d2) end, + 'E685: Internal error: hash_add()')) + eq(2, d2.dv_refcount) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_dict(d, 'testt', 3, d2) end, + nil)) + eq(2, d2.dv_refcount) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('nr()', function() + itp('works', function() + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_nr(d, 'testt', 3, 2)) + local dis = dict_items(d) + alloc_log:check({a.di(dis.tes, 'tes')}) + eq({test=10, tes=int(2)}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_nr(d, 'testt', 3, 2) end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_nr(d, 'testt', 3, 2) end, + nil)) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + describe('str()', function() + itp('works', function() + local d = dict({test=10}) + alloc_log:clear() + eq({test=10}, dct2tbl(d)) + eq(OK, lib.tv_dict_add_str(d, 'testt', 3, 'TEST')) + local dis = dict_items(d) + alloc_log:check({ + a.di(dis.tes, 'tes'), + a.str(dis.tes.di_tv.vval.v_string, 'TEST') + }) + eq({test=10, tes='TEST'}, dct2tbl(d)) + eq(FAIL, check_emsg(function() return lib.tv_dict_add_str(d, 'testt', 3, 'TEST') end, + 'E685: Internal error: hash_add()')) + alloc_log:clear() + lib.emsg_skip = lib.emsg_skip + 1 + eq(FAIL, check_emsg(function() return lib.tv_dict_add_str(d, 'testt', 3, 'TEST') end, + nil)) + lib.emsg_skip = lib.emsg_skip - 1 + alloc_log:clear_tmp_allocs() + alloc_log:check({}) + end) + end) + end) + describe('clear()', function() + itp('works', function() + local d = dict() + alloc_log:check({a.dict(d)}) + eq({}, dct2tbl(d)) + lib.tv_dict_clear(d) + eq({}, dct2tbl(d)) + lib.tv_dict_add_str(d, 'TEST', 3, 'tEsT') + local dis = dict_items(d) + local di = dis.TES + local di_s = di.di_tv.vval.v_string + alloc_log:check({a.di(di), a.str(di_s)}) + eq({TES='tEsT'}, dct2tbl(d)) + lib.tv_dict_clear(d) + alloc_log:check({a.freed(di_s), a.freed(di)}) + eq({}, dct2tbl(d)) + end) + end) + describe('extend()', function() + local function tv_dict_extend(d1, d2, action, emsg) + action = action or "force" + check_emsg(function() return lib.tv_dict_extend(d1, d2, action) end, emsg) + end + itp('works', function() + local d1 = dict() + alloc_log:check({a.dict(d1)}) + eq({}, dct2tbl(d1)) + local d2 = dict() + alloc_log:check({a.dict(d2)}) + eq({}, dct2tbl(d2)) + tv_dict_extend(d1, d2, 'error') + tv_dict_extend(d1, d2, 'keep') + tv_dict_extend(d1, d2, 'force') + alloc_log:check({}) + + d1 = dict({a='TEST'}) + eq({a='TEST'}, dct2tbl(d1)) + local dis1 = dict_items(d1) + local a1_s = dis1.a.di_tv.vval.v_string + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d1), + a.di(dis1.a), + a.str(a1_s), + }) + d2 = dict({a='TSET'}) + eq({a='TSET'}, dct2tbl(d2)) + local dis2 = dict_items(d2) + local a2_s = dis2.a.di_tv.vval.v_string + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d2), + a.di(dis2.a), + a.str(a2_s), + }) + + tv_dict_extend(d1, d2, 'error', 'E737: Key already exists: a') + eq({a='TEST'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + alloc_log:clear() + + tv_dict_extend(d1, d2, 'keep') + alloc_log:check({}) + eq({a='TEST'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + + tv_dict_extend(d1, d2, 'force') + alloc_log:check({ + a.freed(a1_s), + a.str(dis1.a.di_tv.vval.v_string), + }) + eq({a='TSET'}, dct2tbl(d1)) + eq({a='TSET'}, dct2tbl(d2)) + end) + itp('disallows overriding builtin or user functions', function() + local d = dict() + d.dv_scope = lib.VAR_DEF_SCOPE + local f_lua = { + [type_key]=func_type, + value='tr', + } + local f_tv = lua2typvalt(f_lua) + local p_lua = { + [type_key]=func_type, + value='tr', + args={1}, + } + local p_tv = lua2typvalt(p_lua) + eq(lib.VAR_PARTIAL, p_tv.v_type) + local d2 = dict({tr=f_tv}) + local d3 = dict({tr=p_tv}) + local d4 = dict({['TEST:THIS']=p_tv}) + local d5 = dict({Test=f_tv}) + local d6 = dict({Test=p_tv}) + eval0([[execute("function Test()\nendfunction")]]) + tv_dict_extend(d, d2, 'force', + 'E704: Funcref variable name must start with a capital: tr') + tv_dict_extend(d, d3, 'force', + 'E704: Funcref variable name must start with a capital: tr') + tv_dict_extend(d, d4, 'force', + 'E461: Illegal variable name: TEST:THIS') + tv_dict_extend(d, d5, 'force', + 'E705: Variable name conflicts with existing function: Test') + tv_dict_extend(d, d6, 'force', + 'E705: Variable name conflicts with existing function: Test') + eq({}, dct2tbl(d)) + d.dv_scope = lib.VAR_SCOPE + tv_dict_extend(d, d4, 'force', + 'E461: Illegal variable name: TEST:THIS') + eq({}, dct2tbl(d)) + tv_dict_extend(d, d2, 'force') + eq({tr=f_lua}, dct2tbl(d)) + tv_dict_extend(d, d3, 'force') + eq({tr=p_lua}, dct2tbl(d)) + tv_dict_extend(d, d5, 'force') + eq({tr=p_lua, Test=f_lua}, dct2tbl(d)) + tv_dict_extend(d, d6, 'force') + eq({tr=p_lua, Test=p_lua}, dct2tbl(d)) + end) + itp('cares about locks and read-only items', function() + local d_lua = {tv_locked=1, tv_fixed=2, di_ro=3, di_ro_sbx=4} + local d = dict(d_lua) + local dis = dict_items(d) + dis.tv_locked.di_tv.v_lock = lib.VAR_LOCKED + dis.tv_fixed.di_tv.v_lock = lib.VAR_FIXED + dis.di_ro.di_flags = bit.bor(dis.di_ro.di_flags, lib.DI_FLAGS_RO) + dis.di_ro_sbx.di_flags = bit.bor(dis.di_ro_sbx.di_flags, lib.DI_FLAGS_RO_SBX) + lib.sandbox = true + local d1 = dict({tv_locked=41}) + local d2 = dict({tv_fixed=42}) + local d3 = dict({di_ro=43}) + local d4 = dict({di_ro_sbx=44}) + tv_dict_extend(d, d1, 'force', 'E741: Value is locked: extend() argument') + tv_dict_extend(d, d2, 'force', 'E742: Cannot change value of extend() argument') + tv_dict_extend(d, d3, 'force', 'E46: Cannot change read-only variable "extend() argument"') + tv_dict_extend(d, d4, 'force', 'E794: Cannot set variable in the sandbox: "extend() argument"') + eq(d_lua, dct2tbl(d)) + lib.sandbox = false + tv_dict_extend(d, d4, 'force') + d_lua.di_ro_sbx = 44 + eq(d_lua, dct2tbl(d)) + end) + end) + describe('equal()', function() + local function tv_dict_equal(d1, d2, ic, recursive) + return lib.tv_dict_equal(d1, d2, ic or false, recursive or false) + end + itp('works', function() + eq(true, tv_dict_equal(nil, nil)) + local d1 = dict() + alloc_log:check({a.dict(d1)}) + eq(1, d1.dv_refcount) + eq(false, tv_dict_equal(nil, d1)) + eq(false, tv_dict_equal(d1, nil)) + eq(true, tv_dict_equal(d1, d1)) + eq(1, d1.dv_refcount) + alloc_log:check({}) + local d_upper = dict({a='TEST'}) + local dis_upper = dict_items(d_upper) + local d_lower = dict({a='test'}) + local dis_lower = dict_items(d_lower) + local d_kupper_upper = dict({A='TEST'}) + local dis_kupper_upper = dict_items(d_kupper_upper) + local d_kupper_lower = dict({A='test'}) + local dis_kupper_lower = dict_items(d_kupper_lower) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d_upper), + a.di(dis_upper.a), + a.str(dis_upper.a.di_tv.vval.v_string), + + a.dict(d_lower), + a.di(dis_lower.a), + a.str(dis_lower.a.di_tv.vval.v_string), + + a.dict(d_kupper_upper), + a.di(dis_kupper_upper.A), + a.str(dis_kupper_upper.A.di_tv.vval.v_string), + + a.dict(d_kupper_lower), + a.di(dis_kupper_lower.A), + a.str(dis_kupper_lower.A.di_tv.vval.v_string), + }) + eq(true, tv_dict_equal(d_upper, d_upper)) + eq(true, tv_dict_equal(d_upper, d_upper, true)) + eq(false, tv_dict_equal(d_upper, d_lower, false)) + eq(true, tv_dict_equal(d_upper, d_lower, true)) + eq(true, tv_dict_equal(d_kupper_upper, d_kupper_lower, true)) + eq(false, tv_dict_equal(d_kupper_upper, d_lower, true)) + eq(false, tv_dict_equal(d_kupper_upper, d_upper, true)) + eq(true, tv_dict_equal(d_upper, d_upper, true, true)) + alloc_log:check({}) + end) + end) + describe('copy()', function() + local function tv_dict_copy(...) + return ffi.gc(lib.tv_dict_copy(...), lib.tv_dict_unref) + end + itp('copies NULL correctly', function() + eq(nil, lib.tv_dict_copy(nil, nil, true, 0)) + eq(nil, lib.tv_dict_copy(nil, nil, false, 0)) + eq(nil, lib.tv_dict_copy(nil, nil, true, 1)) + eq(nil, lib.tv_dict_copy(nil, nil, false, 1)) + end) + itp('copies dict correctly without converting items', function() + do + local v = {a={['«']='»'}, b={'„'}, ['1']=1, ['«»']='“', ns=null_string, nl=null_list, nd=null_dict} + local d_tv = lua2typvalt(v) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_copy1 = tv_dict_copy(nil, d, false, 0) + eq(2, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(2, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_copy1 = dict_items(d_copy1) + eq(dis.a.di_tv.vval.v_dict, dis_copy1.a.di_tv.vval.v_dict) + eq(dis.b.di_tv.vval.v_list, dis_copy1.b.di_tv.vval.v_list) + eq(v, dct2tbl(d_copy1)) + alloc_log:clear() + lib.tv_dict_free(ffi.gc(d_copy1, nil)) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_deepcopy1 = tv_dict_copy(nil, d, true, 0) + neq(nil, d_deepcopy1) + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_deepcopy1 = dict_items(d_deepcopy1) + neq(dis.a.di_tv.vval.v_dict, dis_deepcopy1.a.di_tv.vval.v_dict) + neq(dis.b.di_tv.vval.v_list, dis_deepcopy1.b.di_tv.vval.v_list) + eq(v, dct2tbl(d_deepcopy1)) + alloc_log:clear() + end + collectgarbage() + end) + itp('copies dict correctly and converts items', function() + local vc = ffi.gc(ffi.new('vimconv_T[1]'), function(vc) + lib.convert_setup(vc, nil, nil) + end) + -- UTF-8 ↔ latin1 conversions need no iconv + eq(OK, lib.convert_setup(vc, to_cstr('utf-8'), to_cstr('latin1'))) + + local v = {a={['«']='»'}, b={'„'}, ['1']=1, ['«»']='“', ns=null_string, nl=null_list, nd=null_dict} + local d_tv = lua2typvalt(v) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + alloc_log:clear() + + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local d_deepcopy1 = tv_dict_copy(vc, d, true, 0) + neq(nil, d_deepcopy1) + eq(1, dis.a.di_tv.vval.v_dict.dv_refcount) + eq(1, dis.b.di_tv.vval.v_list.lv_refcount) + local dis_deepcopy1 = dict_items(d_deepcopy1) + neq(dis.a.di_tv.vval.v_dict, dis_deepcopy1.a.di_tv.vval.v_dict) + neq(dis.b.di_tv.vval.v_list, dis_deepcopy1.b.di_tv.vval.v_list) + eq({a={['\171']='\187'}, b={'\191'}, ['1']=1, ['\171\187']='\191', ns=null_string, nl=null_list, nd=null_dict}, + dct2tbl(d_deepcopy1)) + alloc_log:clear_tmp_allocs() + alloc_log:clear() + end) + itp('returns different/same containers with(out) copyID', function() + local d_inner_tv = lua2typvalt({}) + local d_tv = lua2typvalt({a=d_inner_tv, b=d_inner_tv}) + eq(3, d_inner_tv.vval.v_dict.dv_refcount) + local d = d_tv.vval.v_dict + local dis = dict_items(d) + eq(dis.a.di_tv.vval.v_dict, dis.b.di_tv.vval.v_dict) + + local d_copy1 = tv_dict_copy(nil, d, true, 0) + local dis_copy1 = dict_items(d_copy1) + neq(dis_copy1.a.di_tv.vval.v_dict, dis_copy1.b.di_tv.vval.v_dict) + eq({a={}, b={}}, dct2tbl(d_copy1)) + + local d_copy2 = tv_dict_copy(nil, d, true, 2) + local dis_copy2 = dict_items(d_copy2) + eq(dis_copy2.a.di_tv.vval.v_dict, dis_copy2.b.di_tv.vval.v_dict) + eq({a={}, b={}}, dct2tbl(d_copy2)) + + eq(3, d_inner_tv.vval.v_dict.dv_refcount) + end) + itp('works with self-referencing dict with copyID', function() + local d_tv = lua2typvalt({}) + local d = d_tv.vval.v_dict + eq(1, d.dv_refcount) + lib.tv_dict_add_dict(d, 'test', 4, d) + eq(2, d.dv_refcount) + + local d_copy1 = tv_dict_copy(nil, d, true, 2) + eq(2, d_copy1.dv_refcount) + local v = {} + v.test = v + eq(v, dct2tbl(d_copy1)) + + lib.tv_dict_clear(d) + eq(1, d.dv_refcount) + + lib.tv_dict_clear(d_copy1) + eq(1, d_copy1.dv_refcount) + end) + end) + describe('set_keys_readonly()', function() + itp('works', function() + local d = dict({a=true}) + local dis = dict_items(d) + alloc_log:check({a.dict(d), a.di(dis.a)}) + eq(0, bit.band(dis.a.di_flags, lib.DI_FLAGS_RO)) + eq(0, bit.band(dis.a.di_flags, lib.DI_FLAGS_FIX)) + lib.tv_dict_set_keys_readonly(d) + alloc_log:check({}) + eq(lib.DI_FLAGS_RO, bit.band(dis.a.di_flags, lib.DI_FLAGS_RO)) + eq(lib.DI_FLAGS_FIX, bit.band(dis.a.di_flags, lib.DI_FLAGS_FIX)) + end) + end) + end) + describe('tv', function() + describe('alloc', function() + describe('list ret()', function() + itp('works', function() + local rettv = typvalt(lib.VAR_UNKNOWN) + local l = lib.tv_list_alloc_ret(rettv) + eq(empty_list, typvalt2lua(rettv)) + eq(rettv.vval.v_list, l) + end) + end) + describe('dict ret()', function() + itp('works', function() + local rettv = typvalt(lib.VAR_UNKNOWN) + lib.tv_dict_alloc_ret(rettv) + eq({}, typvalt2lua(rettv)) + end) + end) + end) + local function defalloc() + return {} + end + describe('clear()', function() + itp('works', function() + local function deffrees(alloc_rets) + local ret = {} + for i = #alloc_rets, 1, -1 do + ret[#alloc_rets - i + 1] = alloc_rets:freed(i) + end + return ret + end + alloc_log:check({}) + lib.tv_clear(nil) + alloc_log:check({}) + local ll = {} + local ll_l = nil + ll[1] = ll + local dd = {} + local dd_d = nil + dd.dd = dd + for _, v in ipairs({ + {nil_value}, + {null_string, nil, function() return {a.freed(alloc_log.null)} end}, + {0}, + {int(0)}, + {true}, + {false}, + {'true', function(tv) return {a.str(tv.vval.v_string)} end}, + {{}, function(tv) return {a.dict(tv.vval.v_dict)} end}, + {empty_list, function(tv) return {a.list(tv.vval.v_list)} end}, + {ll, function(tv) + ll_l = tv.vval.v_list + return {a.list(tv.vval.v_list), a.li(tv.vval.v_list.lv_first)} + end, defalloc}, + {dd, function(tv) + dd_d = tv.vval.v_dict + return {a.dict(tv.vval.v_dict), a.di(first_di(tv.vval.v_dict))} + end, defalloc}, + }) do + local tv = lua2typvalt(v[1]) + local alloc_rets = {} + alloc_log:check(get_alloc_rets((v[2] or defalloc)(tv), alloc_rets)) + lib.tv_clear(tv) + alloc_log:check((v[3] or deffrees)(alloc_rets)) + end + eq(1, ll_l.lv_refcount) + eq(1, dd_d.dv_refcount) + end) + end) + describe('copy()', function() + itp('works', function() + local function strallocs(tv) + return {a.str(tv.vval.v_string)} + end + for _, v in ipairs({ + {nil_value}, + {null_string}, + {0}, + {int(0)}, + {true}, + {false}, + {{}, function(tv) return {a.dict(tv.vval.v_dict)} end, nil, function(from, to) + eq(2, to.vval.v_dict.dv_refcount) + eq(to.vval.v_dict, from.vval.v_dict) + end}, + {empty_list, function(tv) return {a.list(tv.vval.v_list)} end, nil, function(from, to) + eq(2, to.vval.v_list.lv_refcount) + eq(to.vval.v_list, from.vval.v_list) + end}, + {'test', strallocs, strallocs, function(from, to) + neq(to.vval.v_string, from.vval.v_string) + end}, + }) do + local from = lua2typvalt(v[1]) + alloc_log:check((v[2] or defalloc)(from)) + local to = typvalt(lib.VAR_UNKNOWN) + lib.tv_copy(from, to) + local res = v[1] + eq(res, typvalt2lua(to)) + alloc_log:check((v[3] or defalloc)(to)) + if v[4] then + v[4](from, to) + end + end + end) + end) + describe('item_lock()', function() + itp('does not alter VAR_PARTIAL', function() + local p_tv = lua2typvalt({ + [type_key]=func_type, + value='tr', + dict={}, + }) + lib.tv_item_lock(p_tv, -1, true) + eq(lib.VAR_UNLOCKED, p_tv.vval.v_partial.pt_dict.dv_lock) + end) + itp('does not change VAR_FIXED values', function() + local d_tv = lua2typvalt({}) + local l_tv = lua2typvalt(empty_list) + alloc_log:clear() + d_tv.v_lock = lib.VAR_FIXED + d_tv.vval.v_dict.dv_lock = lib.VAR_FIXED + l_tv.v_lock = lib.VAR_FIXED + l_tv.vval.v_list.lv_lock = lib.VAR_FIXED + lib.tv_item_lock(d_tv, 1, true) + lib.tv_item_lock(l_tv, 1, true) + eq(lib.VAR_FIXED, d_tv.v_lock) + eq(lib.VAR_FIXED, l_tv.v_lock) + eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) + eq(lib.VAR_FIXED, l_tv.vval.v_list.lv_lock) + lib.tv_item_lock(d_tv, 1, false) + lib.tv_item_lock(l_tv, 1, false) + eq(lib.VAR_FIXED, d_tv.v_lock) + eq(lib.VAR_FIXED, l_tv.v_lock) + eq(lib.VAR_FIXED, d_tv.vval.v_dict.dv_lock) + eq(lib.VAR_FIXED, l_tv.vval.v_list.lv_lock) + alloc_log:check({}) + end) + itp('works with NULL values', function() + local l_tv = lua2typvalt(null_list) + local d_tv = lua2typvalt(null_dict) + local s_tv = lua2typvalt(null_string) + alloc_log:clear() + lib.tv_item_lock(l_tv, 1, true) + lib.tv_item_lock(d_tv, 1, true) + lib.tv_item_lock(s_tv, 1, true) + eq(null_list, typvalt2lua(l_tv)) + eq(null_dict, typvalt2lua(d_tv)) + eq(null_string, typvalt2lua(s_tv)) + eq(lib.VAR_LOCKED, d_tv.v_lock) + eq(lib.VAR_LOCKED, l_tv.v_lock) + eq(lib.VAR_LOCKED, s_tv.v_lock) + alloc_log:check({}) + end) + end) + describe('islocked()', function() + itp('works with NULL values', function() + local l_tv = lua2typvalt(null_list) + local d_tv = lua2typvalt(null_dict) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + end) + itp('works', function() + local tv = lua2typvalt() + local d_tv = lua2typvalt({}) + local l_tv = lua2typvalt(empty_list) + alloc_log:clear() + eq(false, lib.tv_islocked(tv)) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_LOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + tv.v_lock = lib.VAR_LOCKED + d_tv.v_lock = lib.VAR_LOCKED + l_tv.v_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(tv)) + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_UNLOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_UNLOCKED + eq(true, lib.tv_islocked(tv)) + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + tv.v_lock = lib.VAR_FIXED + d_tv.v_lock = lib.VAR_FIXED + l_tv.v_lock = lib.VAR_FIXED + eq(false, lib.tv_islocked(tv)) + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_LOCKED + l_tv.vval.v_list.lv_lock = lib.VAR_LOCKED + eq(true, lib.tv_islocked(l_tv)) + eq(true, lib.tv_islocked(d_tv)) + d_tv.vval.v_dict.dv_lock = lib.VAR_FIXED + l_tv.vval.v_list.lv_lock = lib.VAR_FIXED + eq(false, lib.tv_islocked(l_tv)) + eq(false, lib.tv_islocked(d_tv)) + alloc_log:check({}) + end) + end) + describe('check_lock()', function() + local function tv_check_lock(lock, name, name_len, emsg) + return check_emsg(function() + return lib.tv_check_lock(lock, name, name_len) + end, emsg) + end + itp('works', function() + eq(false, tv_check_lock(lib.VAR_UNLOCKED, 'test', 3)) + eq(true, tv_check_lock(lib.VAR_LOCKED, 'test', 3, + 'E741: Value is locked: tes')) + eq(true, tv_check_lock(lib.VAR_FIXED, 'test', 3, + 'E742: Cannot change value of tes')) + eq(true, tv_check_lock(lib.VAR_LOCKED, nil, 0, + 'E741: Value is locked: Unknown')) + eq(true, tv_check_lock(lib.VAR_FIXED, nil, 0, + 'E742: Cannot change value of Unknown')) + end) + end) + describe('equal()', function() + itp('compares empty and NULL lists correctly', function() + local l = lua2typvalt(empty_list) + local l2 = lua2typvalt(empty_list) + local nl = lua2typvalt(null_list) + + -- NULL lists are not equal to empty lists + eq(false, lib.tv_equal(l, nl, true, false)) + eq(false, lib.tv_equal(nl, l, false, false)) + eq(false, lib.tv_equal(nl, l, false, true)) + eq(false, lib.tv_equal(l, nl, true, true)) + + -- Yet NULL lists are equal themselves + eq(true, lib.tv_equal(nl, nl, true, false)) + eq(true, lib.tv_equal(nl, nl, false, false)) + eq(true, lib.tv_equal(nl, nl, false, true)) + eq(true, lib.tv_equal(nl, nl, true, true)) + + -- As well as empty lists + eq(true, lib.tv_equal(l, l, true, false)) + eq(true, lib.tv_equal(l, l2, false, false)) + eq(true, lib.tv_equal(l2, l, false, true)) + eq(true, lib.tv_equal(l2, l2, true, true)) + end) + -- Must not use recursive=true argument in the following tests because it + -- indicates that tv_equal_recurse_limit and recursive_cnt were set which + -- is essential. This argument will be set when comparing inner lists. + itp('compares lists correctly when case is not ignored', function() + local l1 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l2 = lua2typvalt({'abc', {1, 2, 'Abc'}}) + local l3 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'Def'}) + local l4 = lua2typvalt({'abc', {1, 2, 'Abc', 4}, 'def'}) + local l5 = lua2typvalt({'Abc', {1, 2, 'Abc'}, 'def'}) + local l6 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l7 = lua2typvalt({'abc', {1, 2, 'abc'}, 'def'}) + local l8 = lua2typvalt({'abc', nil, 'def'}) + local l9 = lua2typvalt({'abc', {1, 2, nil}, 'def'}) + + eq(true, lib.tv_equal(l1, l1, false, false)) + eq(false, lib.tv_equal(l1, l2, false, false)) + eq(false, lib.tv_equal(l1, l3, false, false)) + eq(false, lib.tv_equal(l1, l4, false, false)) + eq(false, lib.tv_equal(l1, l5, false, false)) + eq(true, lib.tv_equal(l1, l6, false, false)) + eq(false, lib.tv_equal(l1, l7, false, false)) + eq(false, lib.tv_equal(l1, l8, false, false)) + eq(false, lib.tv_equal(l1, l9, false, false)) + end) + itp('compares lists correctly when case is ignored', function() + local l1 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l2 = lua2typvalt({'abc', {1, 2, 'Abc'}}) + local l3 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'Def'}) + local l4 = lua2typvalt({'abc', {1, 2, 'Abc', 4}, 'def'}) + local l5 = lua2typvalt({'Abc', {1, 2, 'Abc'}, 'def'}) + local l6 = lua2typvalt({'abc', {1, 2, 'Abc'}, 'def'}) + local l7 = lua2typvalt({'abc', {1, 2, 'abc'}, 'def'}) + local l8 = lua2typvalt({'abc', nil, 'def'}) + local l9 = lua2typvalt({'abc', {1, 2, nil}, 'def'}) + + eq(true, lib.tv_equal(l1, l1, true, false)) + eq(false, lib.tv_equal(l1, l2, true, false)) + eq(true, lib.tv_equal(l1, l3, true, false)) + eq(false, lib.tv_equal(l1, l4, true, false)) + eq(true, lib.tv_equal(l1, l5, true, false)) + eq(true, lib.tv_equal(l1, l6, true, false)) + eq(true, lib.tv_equal(l1, l7, true, false)) + eq(false, lib.tv_equal(l1, l8, true, false)) + eq(false, lib.tv_equal(l1, l9, true, false)) + end) + local function tv_equal(d1, d2, ic, recursive) + return lib.tv_equal(d1, d2, ic or false, recursive or false) + end + itp('works with dictionaries', function() + local nd = lua2typvalt(null_dict) + eq(true, tv_equal(nd, nd)) + alloc_log:check({}) + local d1 = lua2typvalt({}) + alloc_log:check({a.dict(d1.vval.v_dict)}) + eq(1, d1.vval.v_dict.dv_refcount) + eq(false, tv_equal(nd, d1)) + eq(false, tv_equal(d1, nd)) + eq(true, tv_equal(d1, d1)) + eq(1, d1.vval.v_dict.dv_refcount) + alloc_log:check({}) + local d_upper = lua2typvalt({a='TEST'}) + local dis_upper = dict_items(d_upper.vval.v_dict) + local d_lower = lua2typvalt({a='test'}) + local dis_lower = dict_items(d_lower.vval.v_dict) + local d_kupper_upper = lua2typvalt({A='TEST'}) + local dis_kupper_upper = dict_items(d_kupper_upper.vval.v_dict) + local d_kupper_lower = lua2typvalt({A='test'}) + local dis_kupper_lower = dict_items(d_kupper_lower.vval.v_dict) + alloc_log:clear_tmp_allocs() + alloc_log:check({ + a.dict(d_upper.vval.v_dict), + a.di(dis_upper.a), + a.str(dis_upper.a.di_tv.vval.v_string), + + a.dict(d_lower.vval.v_dict), + a.di(dis_lower.a), + a.str(dis_lower.a.di_tv.vval.v_string), + + a.dict(d_kupper_upper.vval.v_dict), + a.di(dis_kupper_upper.A), + a.str(dis_kupper_upper.A.di_tv.vval.v_string), + + a.dict(d_kupper_lower.vval.v_dict), + a.di(dis_kupper_lower.A), + a.str(dis_kupper_lower.A.di_tv.vval.v_string), + }) + eq(true, tv_equal(d_upper, d_upper)) + eq(true, tv_equal(d_upper, d_upper, true)) + eq(false, tv_equal(d_upper, d_lower, false)) + eq(true, tv_equal(d_upper, d_lower, true)) + eq(true, tv_equal(d_kupper_upper, d_kupper_lower, true)) + eq(false, tv_equal(d_kupper_upper, d_lower, true)) + eq(false, tv_equal(d_kupper_upper, d_upper, true)) + eq(true, tv_equal(d_upper, d_upper, true, true)) + alloc_log:check({}) + end) + end) + describe('check', function() + describe('str_or_nr()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E805: Expected a Number or a String, Float found'}, + {lib.VAR_PARTIAL, 'E703: Expected a Number or a String, Funcref found'}, + {lib.VAR_FUNC, 'E703: Expected a Number or a String, Funcref found'}, + {lib.VAR_LIST, 'E745: Expected a Number or a String, List found'}, + {lib.VAR_DICT, 'E728: Expected a Number or a String, Dictionary found'}, + {lib.VAR_SPECIAL, 'E5300: Expected a Number or a String'}, + {lib.VAR_UNKNOWN, 'E685: Internal error: tv_check_str_or_nr(UNKNOWN)'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_str_or_nr(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('num()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E805: Using a Float as a Number'}, + {lib.VAR_PARTIAL, 'E703: Using a Funcref as a Number'}, + {lib.VAR_FUNC, 'E703: Using a Funcref as a Number'}, + {lib.VAR_LIST, 'E745: Using a List as a Number'}, + {lib.VAR_DICT, 'E728: Using a Dictionary as a Number'}, + {lib.VAR_SPECIAL, nil}, + {lib.VAR_UNKNOWN, 'E685: using an invalid value as a Number'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_num(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('str()', function() + itp('works', function() + local tv = typvalt() + local mem = lib.xmalloc(1) + tv.vval.v_list = mem -- Should crash when actually accessed + alloc_log:clear() + for _, v in ipairs({ + {lib.VAR_NUMBER, nil}, + {lib.VAR_FLOAT, 'E806: using Float as a String'}, + {lib.VAR_PARTIAL, 'E729: using Funcref as a String'}, + {lib.VAR_FUNC, 'E729: using Funcref as a String'}, + {lib.VAR_LIST, 'E730: using List as a String'}, + {lib.VAR_DICT, 'E731: using Dictionary as a String'}, + {lib.VAR_SPECIAL, nil}, + {lib.VAR_UNKNOWN, 'E908: using an invalid value as a String'}, + }) do + local typ = v[1] + local emsg = v[2] + local ret = true + if emsg then ret = false end + tv.v_type = typ + eq(ret, check_emsg(function() return lib.tv_check_str(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + end) + describe('get', function() + describe('number()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', 0}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_number(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('number_chk()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', 0}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = {v[4], not not emsg} + eq(ret, check_emsg(function() + local err = ffi.new('bool[1]', {false}) + local res = lib.tv_get_number_chk(tv, err) + return {res, err[0]} + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('lnum()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, 100500}, + {lib.VAR_STRING, {v_string=to_cstr('.')}, nil, 46}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E805: Using a Float as a Number', -1}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E703: Using a Funcref as a Number', -1}, + {lib.VAR_FUNC, {v_string=NULL}, 'E703: Using a Funcref as a Number', -1}, + {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', -1}, + {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', -1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', -1}, + }) do + lib.curwin.w_cursor.lnum = 46 + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_lnum(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('float()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, 42}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, 'E892: Using a String as a Float', 0}, + {lib.VAR_FLOAT, {v_float=42.53}, nil, 42.53}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E891: Using a Funcref as a Float', 0}, + {lib.VAR_FUNC, {v_string=NULL}, 'E891: Using a Funcref as a Float', 0}, + {lib.VAR_LIST, {v_list=NULL}, 'E893: Using a List as a Float', 0}, + {lib.VAR_DICT, {v_dict=NULL}, 'E894: Using a Dictionary as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_float(UNKNOWN)', 0}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() return lib.tv_get_float(tv) end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string()', function() + itp('works', function() + local buf = lib.tv_get_string(lua2typvalt(int(1))) + local buf_chk = lib.tv_get_string_chk(lua2typvalt(int(1))) + neq(buf, buf_chk) + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', ''}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local res = lib.tv_get_string(tv) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_chk()', function() + itp('works', function() + local buf = lib.tv_get_string_chk(lua2typvalt(int(1))) + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', nil}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local res = lib.tv_get_string_chk(tv) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_buf()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', ''}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', ''}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) + local res = lib.tv_get_string_buf(tv, buf) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + describe('string_buf_chk()', function() + itp('works', function() + for _, v in ipairs({ + {lib.VAR_NUMBER, {v_number=42}, nil, '42'}, + {lib.VAR_STRING, {v_string=to_cstr('100500')}, nil, '100500'}, + {lib.VAR_FLOAT, {v_float=42.53}, 'E806: using Float as a String', nil}, + {lib.VAR_PARTIAL, {v_partial=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_FUNC, {v_string=NULL}, 'E729: using Funcref as a String', nil}, + {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, + {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, + {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, + }) do + local tv = typvalt(v[1], v[2]) + alloc_log:check({}) + local emsg = v[3] + local ret = v[4] + eq(ret, check_emsg(function() + local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) + local res = lib.tv_get_string_buf_chk(tv, buf) + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + eq(buf, res) + else + neq(buf, res) + end + if res ~= nil then + return ffi.string(res) + else + return nil + end + end, emsg)) + if emsg then + alloc_log:clear() + else + alloc_log:check({}) + end + end + end) + end) + end) + end) +end) diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index 612b337ee7..74f214a231 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -11,6 +11,7 @@ local posix = nil local syscall = nil local check_cores = global_helpers.check_cores +local dedent = global_helpers.dedent local neq = global_helpers.neq local map = global_helpers.map local eq = global_helpers.eq @@ -314,6 +315,29 @@ local function alloc_log_new() eq(exp, self.log) self:clear() end + function log:clear_tmp_allocs() + local toremove = {} + local allocs = {} + for i, v in ipairs(self.log) do + if v.func == 'malloc' or v.func == 'calloc' then + allocs[tostring(v.ret)] = i + elseif v.func == 'realloc' or v.func == 'free' then + if allocs[tostring(v.args[1])] then + toremove[#toremove + 1] = allocs[tostring(v.args[1])] + if v.func == 'free' then + toremove[#toremove + 1] = i + end + end + if v.func == 'realloc' then + allocs[tostring(v.ret)] = i + end + end + end + table.sort(toremove) + for i = #toremove,1,-1 do + table.remove(self.log, toremove[i]) + end + end function log:restore_original_functions() -- Do nothing: set mocks live in a separate process return @@ -488,6 +512,202 @@ if os.getenv('NVIM_TEST_PRINT_SYSCALLS') == '1' then end end +local function just_fail(_) + return false +end +say:set('assertion.just_fail.positive', '%s') +say:set('assertion.just_fail.negative', '%s') +assert:register('assertion', 'just_fail', just_fail, + 'assertion.just_fail.positive', + 'assertion.just_fail.negative') + +local hook_fnamelen = 30 +local hook_sfnamelen = 30 +local hook_numlen = 5 +local hook_msglen = 1 + 1 + 1 + (1 + hook_fnamelen) + (1 + hook_sfnamelen) + (1 + hook_numlen) + 1 + +local tracehelp = dedent([[ + ┌ Trace type: _r_eturn from function , function _c_all, _l_ine executed, + │ _t_ail return, _C_ount (should not actually appear), + │ _s_aved from previous run for reference. + │┏ Function type: _L_ua function, _C_ function, _m_ain part of chunk, + │┃ function that did _t_ail call. + │┃┌ Function name type: _g_lobal, _l_ocal, _m_ethod, _f_ield, _u_pvalue, + │┃│ space for unknown. + │┃│ ┏ Source file name ┌ Function name ┏ Line + │┃│ ┃ (trunc to 30 bytes, no .lua) │ (truncated to last 30 bytes) ┃ number + CWN SSSSSSSSSSSSSSSSSSSSSSSSSSSSSS:FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF:LLLLL\n +]]) + +local function child_sethook(wr) + local trace_level = os.getenv('NVIM_TEST_TRACE_LEVEL') + if not trace_level or trace_level == '' then + trace_level = 1 + else + trace_level = tonumber(trace_level) + end + if trace_level <= 0 then + return + end + local trace_only_c = trace_level <= 1 + local prev_info, prev_reason, prev_lnum + local function hook(reason, lnum, use_prev) + local info = nil + if use_prev then + info = prev_info + elseif reason ~= 'tail return' then -- tail return + info = debug.getinfo(2, 'nSl') + end + + if trace_only_c and (not info or info.what ~= 'C') and not use_prev then + if info.source:sub(-9) == '_spec.lua' then + prev_info = info + prev_reason = 'saved' + prev_lnum = lnum + end + return + end + if trace_only_c and not use_prev and prev_reason then + hook(prev_reason, prev_lnum, true) + prev_reason = nil + end + + local whatchar = ' ' + local namewhatchar = ' ' + local funcname = '' + local source = '' + local msgchar = reason:sub(1, 1) + + if reason == 'count' then + msgchar = 'C' + end + + if info then + funcname = (info.name or ''):sub(1, hook_fnamelen) + whatchar = info.what:sub(1, 1) + namewhatchar = info.namewhat:sub(1, 1) + if namewhatchar == '' then + namewhatchar = ' ' + end + source = info.source + if source:sub(1, 1) == '@' then + if source:sub(-4, -1) == '.lua' then + source = source:sub(1, -5) + end + source = source:sub(-hook_sfnamelen, -1) + end + lnum = lnum or info.currentline + end + + -- assert(-1 <= lnum and lnum <= 99999) + local lnum_s + if lnum == -1 then + lnum_s = 'nknwn' + else + lnum_s = ('%u'):format(lnum) + end + local msg = ( -- lua does not support %* + '' + .. msgchar + .. whatchar + .. namewhatchar + .. ' ' + .. source .. (' '):rep(hook_sfnamelen - #source) + .. ':' + .. funcname .. (' '):rep(hook_fnamelen - #funcname) + .. ':' + .. ('0'):rep(hook_numlen - #lnum_s) .. lnum_s + .. '\n' + ) + -- eq(hook_msglen, #msg) + sc.write(wr, msg) + end + debug.sethook(hook, 'crl') +end + +local trace_end_msg = ('E%s\n'):format((' '):rep(hook_msglen - 2)) + +local function itp_child(wr, func) + init() + collectgarbage('stop') + child_sethook(wr) + local err, emsg = pcall(func) + debug.sethook() + collectgarbage('restart') + emsg = tostring(emsg) + sc.write(wr, trace_end_msg) + if not err then + if #emsg > 99999 then + emsg = emsg:sub(1, 99999) + end + sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg)) + deinit() + sc.close(wr) + sc.exit(1) + else + sc.write(wr, '+\n') + deinit() + sc.close(wr) + sc.exit(0) + end +end + +local function check_child_err(rd) + local trace = {} + while true do + local traceline = sc.read(rd, hook_msglen) + if #traceline ~= hook_msglen then + if #traceline == 0 then + break + else + trace[#trace + 1] = 'Partial read: <' .. trace .. '>\n' + end + end + if traceline == trace_end_msg then + break + end + trace[#trace + 1] = traceline + end + local res = sc.read(rd, 2) + if #res ~= 2 then + local error + if #trace == 0 then + error = '\nTest crashed, no trace available\n' + else + error = '\nTest crashed, trace:\n' .. tracehelp + for i = 1, #trace do + error = error .. trace[i] + end + end + assert.just_fail(error) + end + if res == '+\n' then + return + end + eq('-\n', res) + local len_s = sc.read(rd, 5) + local len = tonumber(len_s) + neq(0, len) + local err = sc.read(rd, len + 1) + assert.just_fail(err) +end + +local function itp_parent(rd, pid, allow_failure) + local err, emsg = pcall(check_child_err, rd) + sc.wait(pid) + sc.close(rd) + if not err then + if allow_failure then + io.stderr:write('Errorred out:\n' .. tostring(emsg) .. '\n') + os.execute([[ + sh -c "source ci/common/test.sh + check_core_dumps --delete \"]] .. Paths.test_luajit_prg .. [[\""]]) + else + error(emsg) + end + end +end + local function gen_itp(it) child_calls_mod = {} child_calls_mod_once = {} @@ -495,14 +715,6 @@ local function gen_itp(it) preprocess_cache_mod = map(function(v) return v end, preprocess_cache_init) previous_defines_mod = previous_defines_init cdefs_mod = cdefs_init:copy() - local function just_fail(_) - return false - end - say:set('assertion.just_fail.positive', '%s') - say:set('assertion.just_fail.negative', '%s') - assert:register('assertion', 'just_fail', just_fail, - 'assertion.just_fail.positive', - 'assertion.just_fail.negative') local function itp(name, func, allow_failure) if allow_failure and os.getenv('NVIM_TEST_RUN_FAILING_TESTS') ~= '1' then -- FIXME Fix tests with this true @@ -512,50 +724,13 @@ local function gen_itp(it) local rd, wr = sc.pipe() child_pid = sc.fork() if child_pid == 0 then - init() sc.close(rd) - collectgarbage('stop') - local err, emsg = pcall(func) - collectgarbage('restart') - emsg = tostring(emsg) - if not err then - sc.write(wr, ('-\n%05u\n%s'):format(#emsg, emsg)) - deinit() - sc.close(wr) - sc.exit(1) - else - sc.write(wr, '+\n') - deinit() - sc.close(wr) - sc.exit(0) - end + itp_child(wr, func) else sc.close(wr) - sc.wait(child_pid) + local saved_child_pid = child_pid child_pid = nil - local function check() - local res = sc.read(rd, 2) - eq(2, #res) - if res == '+\n' then - return - end - eq('-\n', res) - local len_s = sc.read(rd, 5) - local len = tonumber(len_s) - neq(0, len) - local err = sc.read(rd, len + 1) - assert.just_fail(err) - end - local err, emsg = pcall(check) - sc.close(rd) - if not err then - if allow_failure then - io.stderr:write('Errorred out:\n' .. tostring(emsg) .. '\n') - os.execute([[sh -c "source .ci/common/test.sh ; check_core_dumps --delete \"]] .. Paths.test_luajit_prg .. [[\""]]) - else - error(emsg) - end - end + itp_parent(rd, saved_child_pid, allow_failure) end end) end @@ -587,6 +762,7 @@ local module = { only_separate = only_separate, child_call_once = child_call_once, child_cleanup_once = child_cleanup_once, + sc = sc, } return function(after_each) if after_each then diff --git a/test/unit/os/env_spec.lua b/test/unit/os/env_spec.lua index 1ffed784ff..823b6d6a85 100644 --- a/test/unit/os/env_spec.lua +++ b/test/unit/os/env_spec.lua @@ -35,17 +35,17 @@ describe('env function', function() local OK = 0 itp('sets an env variable and returns OK', function() - local name = 'NEOVIM_UNIT_TEST_SETENV_1N' - local value = 'NEOVIM_UNIT_TEST_SETENV_1V' + local name = 'NVIM_UNIT_TEST_SETENV_1N' + local value = 'NVIM_UNIT_TEST_SETENV_1V' eq(nil, os.getenv(name)) eq(OK, (os_setenv(name, value, 1))) eq(value, os.getenv(name)) end) itp("dosn't overwrite an env variable if overwrite is 0", function() - local name = 'NEOVIM_UNIT_TEST_SETENV_2N' - local value = 'NEOVIM_UNIT_TEST_SETENV_2V' - local value_updated = 'NEOVIM_UNIT_TEST_SETENV_2V_UPDATED' + local name = 'NVIM_UNIT_TEST_SETENV_2N' + local value = 'NVIM_UNIT_TEST_SETENV_2V' + local value_updated = 'NVIM_UNIT_TEST_SETENV_2V_UPDATED' eq(OK, (os_setenv(name, value, 0))) eq(value, os.getenv(name)) eq(OK, (os_setenv(name, value_updated, 0))) @@ -69,8 +69,8 @@ describe('env function', function() describe('os_getenv', function() itp('reads an env variable', function() - local name = 'NEOVIM_UNIT_TEST_GETENV_1N' - local value = 'NEOVIM_UNIT_TEST_GETENV_1V' + local name = 'NVIM_UNIT_TEST_GETENV_1N' + local value = 'NVIM_UNIT_TEST_GETENV_1V' eq(NULL, os_getenv(name)) -- need to use os_setenv, because lua dosn't have a setenv function os_setenv(name, value, 1) @@ -78,7 +78,7 @@ describe('env function', function() end) itp('returns NULL if the env variable is not found', function() - local name = 'NEOVIM_UNIT_TEST_GETENV_NOTFOUND' + local name = 'NVIM_UNIT_TEST_GETENV_NOTFOUND' return eq(NULL, os_getenv(name)) end) end) @@ -97,8 +97,8 @@ describe('env function', function() describe('os_getenvname_at_index', function() itp('returns names of environment variables', function() - local test_name = 'NEOVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1N' - local test_value = 'NEOVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1V' + local test_name = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1N' + local test_value = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1V' os_setenv(test_name, test_value, 1) local i = 0 local names = { } @@ -160,16 +160,16 @@ describe('env function', function() describe('expand_env_esc', function() itp('expands environment variables', function() - local name = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN' - local value = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV' + local name = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCN' + local value = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV' os_setenv(name, value, 1) -- TODO(bobtwinkles) This only tests Unix expansions. There should be a -- test for Windows as well - local input1 = to_cstr('$NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN/test') - local input2 = to_cstr('${NEOVIM_UNIT_TEST_EXPAND_ENV_ESCN}/test') + local input1 = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESCN/test') + local input2 = to_cstr('${NVIM_UNIT_TEST_EXPAND_ENV_ESCN}/test') local output_buff1 = cstr(255, '') local output_buff2 = cstr(255, '') - local output_expected = 'NEOVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' + local output_expected = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV/test' cimp.expand_env_esc(input1, output_buff1, 255, false, true, NULL) cimp.expand_env_esc(input2, output_buff2, 255, false, true, NULL) eq(output_expected, ffi.string(output_buff1)) diff --git a/test/unit/os/fileio_spec.lua b/test/unit/os/fileio_spec.lua index 7a738ce85c..e3c8e616ce 100644 --- a/test/unit/os/fileio_spec.lua +++ b/test/unit/os/fileio_spec.lua @@ -80,6 +80,10 @@ local function file_read(fp, size) return ret1, ret2 end +local function file_flush(fp) + return m.file_flush(fp) +end + local function file_fsync(fp) return m.file_fsync(fp) end @@ -94,7 +98,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rwx------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rw------- file with kFileCreate', function() @@ -102,7 +106,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rw-------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rwx------ file with kFileCreateOnly', function() @@ -110,7 +114,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rwx------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can create a rw------- file with kFileCreateOnly', function() @@ -118,7 +122,7 @@ describe('file_open', function() eq(0, err) local attrs = lfs.attributes(filec) eq('rw-------', attrs.permissions) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('fails to open an existing file with kFileCreateOnly', function() @@ -137,35 +141,35 @@ describe('file_open', function() local err, fp = file_open(file1, m.kFileCreate, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can open an existing file read-only with zero', function() local err, fp = file_open(file1, 0, 384) eq(0, err) eq(false, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can open an existing file read-only with kFileReadOnly', function() local err, fp = file_open(file1, m.kFileReadOnly, 384) eq(0, err) eq(false, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can open an existing file read-only with kFileNoSymlink', function() local err, fp = file_open(file1, m.kFileNoSymlink, 384) eq(0, err) eq(false, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can truncate an existing file with kFileTruncate', function() local err, fp = file_open(file1, m.kFileTruncate, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(0, attrs.size) end) @@ -174,7 +178,7 @@ describe('file_open', function() local err, fp = file_open(file1, m.kFileWriteOnly, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(4096, attrs.size) end) @@ -191,7 +195,7 @@ describe('file_open', function() local err, fp = file_open(linkf, m.kFileTruncate, 384) eq(0, err) eq(true, fp.wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) local attrs = lfs.attributes(file1) eq(0, attrs.size) end) @@ -217,7 +221,7 @@ describe('file_open_new', function() local err, fp = file_open_new(file1, 0, 384) eq(0, err) eq(false, fp.wr) - eq(0, m.file_free(fp)) + eq(0, m.file_free(fp, false)) end) itp('fails to open an existing file with kFileCreateOnly', function() @@ -227,7 +231,29 @@ describe('file_open_new', function() end) end) --- file_close is called above, so it is not tested directly +describe('file_close', function() + itp('can flush writes to disk also with true argument', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, err) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, m.file_close(fp, true)) + eq(wsize, lfs.attributes(filec).size) + end) +end) + +describe('file_free', function() + itp('can flush writes to disk also with true argument', function() + local err, fp = file_open_new(filec, m.kFileCreateOnly, 384) + eq(0, err) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, m.file_free(fp, true)) + eq(wsize, lfs.attributes(filec).size) + end) +end) describe('file_fsync', function() itp('can flush writes to disk', function() @@ -240,7 +266,22 @@ describe('file_fsync', function() eq(0, lfs.attributes(filec).size) eq(0, file_fsync(fp)) eq(wsize, lfs.attributes(filec).size) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) + end) +end) + +describe('file_flush', function() + itp('can flush writes to disk', function() + local err, fp = file_open(filec, m.kFileCreateOnly, 384) + eq(0, file_flush(fp)) + eq(0, err) + eq(0, lfs.attributes(filec).size) + local wsize = file_write(fp, 'test') + eq(4, wsize) + eq(0, lfs.attributes(filec).size) + eq(0, file_flush(fp)) + eq(wsize, lfs.attributes(filec).size) + eq(0, m.file_close(fp, false)) end) end) @@ -262,7 +303,7 @@ describe('file_read', function() eq({exp_err, exp_s}, {file_read(fp, size)}) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can read the whole file at once', function() @@ -271,7 +312,7 @@ describe('file_read', function() eq(false, fp.wr) eq({#fcontents, fcontents}, {file_read(fp, #fcontents)}) eq({0, ('\0'):rep(#fcontents)}, {file_read(fp, #fcontents)}) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can read more then 1024 bytes after reading a small chunk', function() @@ -281,7 +322,7 @@ describe('file_read', function() eq({5, fcontents:sub(1, 5)}, {file_read(fp, 5)}) eq({#fcontents - 5, fcontents:sub(6) .. (('\0'):rep(5))}, {file_read(fp, #fcontents)}) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) itp('can read file by 768-byte-chunks', function() @@ -301,7 +342,7 @@ describe('file_read', function() eq({exp_err, exp_s}, {file_read(fp, size)}) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) end) @@ -312,7 +353,7 @@ describe('file_write', function() eq(true, fp.wr) local wr = file_write(fp, fcontents) eq(#fcontents, wr) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(wr, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) @@ -329,7 +370,7 @@ describe('file_write', function() eq(wr, #s) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(#fcontents, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) @@ -346,7 +387,7 @@ describe('file_write', function() eq(wr, #s) shift = shift + size end - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) eq(#fcontents, lfs.attributes(filec).size) eq(fcontents, io.open(filec):read('*a')) end) @@ -361,6 +402,6 @@ describe('file_skip', function() local rd, s = file_read(fp, 3) eq(3, rd) eq(fcontents:sub(4, 6), s) - eq(0, m.file_close(fp)) + eq(0, m.file_close(fp, false)) end) end) diff --git a/test/unit/testtest_spec.lua b/test/unit/testtest_spec.lua new file mode 100644 index 0000000000..d2f3632b6f --- /dev/null +++ b/test/unit/testtest_spec.lua @@ -0,0 +1,19 @@ +local helpers = require('test.unit.helpers')(after_each) +local assert = require('luassert') + +local itp = helpers.gen_itp(it) + +local sc = helpers.sc + +-- All of the below tests must fail. Check how exactly they fail. +if os.getenv('NVIM_TEST_RUN_TESTTEST') ~= '1' then + return +end +describe('test code', function() + itp('does not hang when working with lengthy errors', function() + assert.just_fail(('x'):rep(65536)) + end) + itp('shows trace after exiting abnormally', function() + sc.exit(0) + end) +end) |