diff options
Diffstat (limited to 'test')
23 files changed, 1017 insertions, 275 deletions
diff --git a/test/functional/eval/execute_spec.lua b/test/functional/eval/execute_spec.lua index fc13c0a72b..cc9b61b842 100644 --- a/test/functional/eval/execute_spec.lua +++ b/test/functional/eval/execute_spec.lua @@ -7,7 +7,7 @@ local redir_exec = helpers.redir_exec local exc_exec = helpers.exc_exec local funcs = helpers.funcs local Screen = require('test.functional.ui.screen') -local feed = helpers.feed +local command = helpers.command describe('execute()', function() before_each(clear) @@ -62,11 +62,11 @@ describe('execute()', function() ret = exc_exec('call execute(function("tr"))') eq('Vim(call):E729: using Funcref as a String', ret) ret = exc_exec('call execute(["echo 42", 0.0, "echo 44"])') - eq('Vim(call):E806: using Float as a String', ret) + eq('Vim:E806: using Float as a String', ret) ret = exc_exec('call execute(["echo 42", v:_null_dict, "echo 44"])') - eq('Vim(call):E731: using Dictionary as a String', ret) + eq('Vim:E731: using Dictionary as a String', ret) ret = exc_exec('call execute(["echo 42", function("tr"), "echo 44"])') - eq('Vim(call):E729: using Funcref as a String', ret) + eq('Vim:E729: using Funcref as a String', ret) end) -- This matches Vim behavior. @@ -74,18 +74,75 @@ describe('execute()', function() eq('\n:!echo "foo"\13\n', funcs.execute('!echo "foo"')) end) - it('silences command run inside', function() - local screen = Screen.new(40, 5) - screen:attach() - screen:set_default_attr_ids( {[0] = {bold=true, foreground=255}} ) - feed(':let g:mes = execute("echon 42")<CR>') - screen:expect([[ - ^ | - {0:~ }| - {0:~ }| - {0:~ }| - :let g:mes = execute("echon 42") | - ]]) - eq('42', eval('g:mes')) + describe('{silent} argument', function() + it('captures & displays output for ""', function() + local screen = Screen.new(40, 5) + screen:attach() + command('let g:mes = execute("echon 42", "")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + 42 | + ]]) + eq('42', eval('g:mes')) + end) + + it('captures but does not display output for "silent"', function() + local screen = Screen.new(40, 5) + screen:attach() + command('let g:mes = execute("echon 42")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]]) + eq('42', eval('g:mes')) + + command('let g:mes = execute("echon 13", "silent")') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]]) + eq('13', eval('g:mes')) + end) + + it('suppresses errors for "silent!"', function() + eq(0, exc_exec('let g:mes = execute(0.0, "silent!")')) + eq('', eval('g:mes')) + + eq(0, exc_exec('let g:mes = execute("echon add(1, 1)", "silent!")')) + eq('1', eval('g:mes')) + + eq(0, exc_exec('let g:mes = execute(["echon 42", "echon add(1, 1)"], "silent!")')) + eq('421', eval('g:mes')) + end) + + it('propagates errors for "" and "silent"', function() + local ret + ret = exc_exec('call execute(0.0, "")') + eq('Vim(call):E806: using Float as a String', ret) + + ret = exc_exec('call execute(v:_null_dict, "silent")') + eq('Vim(call):E731: using Dictionary as a String', ret) + + ret = exc_exec('call execute("echo add(1, 1)", "")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute("echo add(1, 1)", "silent")') + eq('Vim(echo):E714: List required', ret) + + ret = exc_exec('call execute(["echon 42", "echo add(1, 1)"], "silent")') + eq('Vim(echo):E714: List required', ret) + end) end) end) diff --git a/test/functional/eval/json_functions_spec.lua b/test/functional/eval/json_functions_spec.lua index 4a6758019b..fc0a19bdfa 100644 --- a/test/functional/eval/json_functions_spec.lua +++ b/test/functional/eval/json_functions_spec.lua @@ -672,6 +672,12 @@ describe('json_encode() function', function() exc_exec('call json_encode(function("tr"))')) end) + it('fails to dump a partial', function() + execute('function T() dict\nendfunction') + eq('Vim(call):E474: Error while dumping encode_tv2json() argument, itself: attempt to dump function reference', + exc_exec('call json_encode(function("T", [1, 2], {}))')) + end) + it('fails to dump a function reference in a list', function() eq('Vim(call):E474: Error while dumping encode_tv2json() argument, index 0: attempt to dump function reference', exc_exec('call json_encode([function("tr")])')) diff --git a/test/functional/eval/msgpack_functions_spec.lua b/test/functional/eval/msgpack_functions_spec.lua index 3fb54c2ee7..44c01d2226 100644 --- a/test/functional/eval/msgpack_functions_spec.lua +++ b/test/functional/eval/msgpack_functions_spec.lua @@ -493,6 +493,12 @@ describe('msgpackparse() function', function() exc_exec('call msgpackparse(function("tr"))')) end) + it('fails to parse a partial', function() + execute('function T() dict\nendfunction') + eq('Vim(call):E686: Argument of msgpackparse() must be a List', + exc_exec('call msgpackparse(function("T", [1, 2], {}))')) + end) + it('fails to parse a float', function() eq('Vim(call):E686: Argument of msgpackparse() must be a List', exc_exec('call msgpackparse(0.0)')) @@ -570,6 +576,13 @@ describe('msgpackdump() function', function() exc_exec('call msgpackdump([Todump])')) end) + it('fails to dump a partial', function() + execute('function T() dict\nendfunction') + execute('let Todump = function("T", [1, 2], {})') + eq('Vim(call):E5004: Error while dumping msgpackdump() argument, index 0, itself: attempt to dump function reference', + exc_exec('call msgpackdump([Todump])')) + end) + it('fails to dump a function reference in a list', function() execute('let todump = [function("tr")]') eq('Vim(call):E5004: Error while dumping msgpackdump() argument, index 0, index 0: attempt to dump function reference', @@ -675,6 +688,12 @@ describe('msgpackdump() function', function() exc_exec('call msgpackdump(function("tr"))')) end) + it('fails to dump a partial', function() + execute('function T() dict\nendfunction') + eq('Vim(call):E686: Argument of msgpackdump() must be a List', + exc_exec('call msgpackdump(function("T", [1, 2], {}))')) + end) + it('fails to dump a float', function() eq('Vim(call):E686: Argument of msgpackdump() must be a List', exc_exec('call msgpackdump(0.0)')) diff --git a/test/functional/eval/string_spec.lua b/test/functional/eval/string_spec.lua index 9e2dc4e111..f6279e85e8 100644 --- a/test/functional/eval/string_spec.lua +++ b/test/functional/eval/string_spec.lua @@ -9,6 +9,8 @@ local redir_exec = helpers.redir_exec local funcs = helpers.funcs local write_file = helpers.write_file local NIL = helpers.NIL +local source = helpers.source +local dedent = helpers.dedent describe('string() function', function() before_each(clear) @@ -110,10 +112,10 @@ describe('string() function', function() function Test1() endfunction - function s:Test2() + function s:Test2() dict endfunction - function g:Test3() + function g:Test3() dict endfunction let g:Test2_f = function('s:Test2') @@ -137,6 +139,85 @@ describe('string() function', function() it('dumps references to script functions', function() eq('function(\'<SNR>1_Test2\')', eval('string(Test2_f)')) end) + + it('dumps partials with self referencing a partial', function() + source([[ + function TestDict() dict + endfunction + let d = {} + let TestDictRef = function('TestDict', d) + let d.tdr = TestDictRef + ]]) + eq("\nE724: unable to correctly dump variable with self-referencing container\nfunction('TestDict', {'tdr': function('TestDict', {E724@1})})", + redir_exec('echo string(d.tdr)')) + end) + + it('dumps automatically created partials', function() + eq('function(\'<SNR>1_Test2\', {\'f\': function(\'<SNR>1_Test2\')})', + eval('string({"f": Test2_f}.f)')) + eq('function(\'<SNR>1_Test2\', [1], {\'f\': function(\'<SNR>1_Test2\', [1])})', + eval('string({"f": function(Test2_f, [1])}.f)')) + end) + + it('dumps manually created partials', function() + eq('function(\'Test3\', [1, 2], {})', + eval('string(function("Test3", [1, 2], {}))')) + eq('function(\'Test3\', {})', + eval('string(function("Test3", {}))')) + eq('function(\'Test3\', [1, 2])', + eval('string(function("Test3", [1, 2]))')) + end) + + it('does not crash or halt when dumping partials with reference cycles in self', + function() + meths.set_var('d', {v=true}) + eq(dedent([[ + + E724: unable to correctly dump variable with self-referencing container + {'p': function('<SNR>1_Test2', {E724@0}), 'f': function('<SNR>1_Test2'), 'v': v:true}]]), + redir_exec('echo string(extend(extend(g:d, {"f": g:Test2_f}), {"p": g:d.f}))')) + end) + + it('does not show errors when dumping partials referencing the same dictionary', + function() + command('let d = {}') + -- Regression for “eval/typval_encode: Dump empty dictionary before + -- checking for refcycle”, results in error. + eq('[function(\'tr\', {}), function(\'tr\', {})]', eval('string([function("tr", d), function("tr", d)])')) + -- Regression for “eval: Work with reference cycles in partials (self) + -- properly”, results in crash. + eval('extend(d, {"a": 1})') + eq('[function(\'tr\', {\'a\': 1}), function(\'tr\', {\'a\': 1})]', eval('string([function("tr", d), function("tr", d)])')) + end) + + it('does not crash or halt when dumping partials with reference cycles in arguments', + function() + meths.set_var('l', {}) + eval('add(l, l)') + -- Regression: the below line used to crash (add returns original list and + -- there was error in dumping partials). Tested explicitly in + -- test/unit/api/private_helpers_spec.lua. + eval('add(l, function("Test1", l))') + eq(dedent([=[ + + E724: unable to correctly dump variable with self-referencing container + function('Test1', [[{E724@2}, function('Test1', [{E724@2}])], function('Test1', [[{E724@4}, function('Test1', [{E724@4}])]])])]=]), + redir_exec('echo string(function("Test1", l))')) + end) + + it('does not crash or halt when dumping partials with reference cycles in self and arguments', + function() + meths.set_var('d', {v=true}) + meths.set_var('l', {}) + eval('add(l, l)') + eval('add(l, function("Test1", l))') + eval('add(l, function("Test1", d))') + eq(dedent([=[ + + E724: unable to correctly dump variable with self-referencing container + {'p': function('<SNR>1_Test2', [[{E724@3}, function('Test1', [{E724@3}]), function('Test1', {E724@0})], function('Test1', [[{E724@5}, function('Test1', [{E724@5}]), function('Test1', {E724@0})]]), function('Test1', {E724@0})], {E724@0}), 'f': function('<SNR>1_Test2'), 'v': v:true}]=]), + redir_exec('echo string(extend(extend(g:d, {"f": g:Test2_f}), {"p": function(g:d.f, l)}))')) + end) end) describe('used to represent lists', function() @@ -174,6 +255,13 @@ describe('string() function', function() eq('{}', eval('string({})')) end) + it('dumps list with two same empty dictionaries, also in partials', function() + command('let d = {}') + eq('[{}, {}]', eval('string([d, d])')) + eq('[function(\'tr\', {}), {}]', eval('string([function("tr", d), d])')) + eq('[{}, function(\'tr\', {})]', eval('string([d, function("tr", d)])')) + end) + it('dumps non-empty dictionary', function() eq('{\'t\'\'est\': 1}', funcs.string({['t\'est']=1})) end) diff --git a/test/functional/ex_cmds/ctrl_c_spec.lua b/test/functional/ex_cmds/ctrl_c_spec.lua new file mode 100644 index 0000000000..b0acb02000 --- /dev/null +++ b/test/functional/ex_cmds/ctrl_c_spec.lua @@ -0,0 +1,62 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear, feed, source = helpers.clear, helpers.feed, helpers.source +local execute = helpers.execute + +if helpers.pending_win32(pending) then return end + +describe("CTRL-C (mapped)", function() + before_each(function() + clear() + end) + + it("interrupts :global", function() + if helpers.skip_fragile(pending, + (os.getenv("TRAVIS") and os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN")) + then + return + end + + source([[ + set nomore nohlsearch undolevels=-1 + nnoremap <C-C> <NOP> + ]]) + + execute("silent edit! test/functional/fixtures/bigfile.txt") + local screen = Screen.new(52, 6) + screen:attach() + screen:set_default_attr_ids({ + [0] = {foreground = Screen.colors.White, + background = Screen.colors.Red}, + [1] = {bold = true, + foreground = Screen.colors.SeaGreen} + }) + + screen:expect([[ + ^0000;<control>;Cc;0;BN;;;;;N;NULL;;;; | + 0001;<control>;Cc;0;BN;;;;;N;START OF HEADING;;;; | + 0002;<control>;Cc;0;BN;;;;;N;START OF TEXT;;;; | + 0003;<control>;Cc;0;BN;;;;;N;END OF TEXT;;;; | + 0004;<control>;Cc;0;BN;;;;;N;END OF TRANSMISSION;;;;| + | + ]]) + + local function test_ctrl_c(ms) + feed(":global/^/p<CR>") + helpers.sleep(ms) + feed("<C-C>") + screen:expect([[Interrupt]], nil, nil, nil, true) + end + + -- The test is time-sensitive. Try different sleep values. + local ms_values = {1, 10, 100} + for i, ms in ipairs(ms_values) do + if i < #ms_values then + local status, _ = pcall(test_ctrl_c, ms) + if status then break end + else -- Call the last attempt directly. + test_ctrl_c(ms) + end + end + end) +end) diff --git a/test/functional/ex_cmds/global_spec.lua b/test/functional/ex_cmds/global_spec.lua deleted file mode 100644 index 81a0ef3248..0000000000 --- a/test/functional/ex_cmds/global_spec.lua +++ /dev/null @@ -1,74 +0,0 @@ -local helpers = require('test.functional.helpers')(after_each) -local Screen = require('test.functional.ui.screen') -local clear, feed, source = helpers.clear, helpers.feed, helpers.source - -if helpers.pending_win32(pending) then return end - -describe(':global', function() - before_each(function() - clear() - end) - - it('is interrupted by mapped CTRL-C', function() - if os.getenv("TRAVIS") and os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN" then - -- XXX: ASAN_UBSAN is too slow to react to the CTRL-C. - pending("", function() end) - return - end - - source([[ - set nomore - set undolevels=-1 - nnoremap <C-C> <NOP> - for i in range(0, 99999) - put ='XXX' - endfor - put ='ZZZ' - 1 - .delete - ]]) - - local screen = Screen.new(52, 6) - screen:attach() - screen:set_default_attr_ids({ - [0] = {foreground = Screen.colors.White, - background = Screen.colors.Red}, - [1] = {bold = true, - foreground = Screen.colors.SeaGreen} - }) - - screen:expect([[ - ^XXX | - XXX | - XXX | - XXX | - XXX | - | - ]]) - - local function test_ctrl_c(ms) - feed(":global/^/p<CR>") - helpers.sleep(ms) - feed("<C-C>") - screen:expect([[ - XXX | - XXX | - XXX | - XXX | - {0:Interrupted} | - Interrupt: {1:Press ENTER or type command to continue}^ | - ]]) - end - - -- The test is time-sensitive. Try with different sleep values. - local ms_values = {10, 50, 100} - for i, ms in ipairs(ms_values) do - if i < #ms_values then - local status, _ = pcall(test_ctrl_c, ms) - if status then break end - else -- Call the last attempt directly. - test_ctrl_c(ms) - end - end - end) -end) diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index f3332cff4f..5eec3afe65 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -14,6 +14,7 @@ local neq = global_helpers.neq local eq = global_helpers.eq local ok = global_helpers.ok +local start_dir = lfs.currentdir() local nvim_prog = os.getenv('NVIM_PROG') or 'build/bin/nvim' local nvim_argv = {nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--cmd', 'set shortmess+=I background=light noswapfile noautoindent laststatus=1 undodir=. directory=. viewdir=. backupdir=.', @@ -21,6 +22,9 @@ local nvim_argv = {nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', local mpack = require('mpack') +local tmpname = global_helpers.tmpname +local uname = global_helpers.uname + -- Formulate a path to the directory containing nvim. We use this to -- help run test executables. It helps to keep the tests working, even -- when the build is not in the default location. @@ -334,44 +338,6 @@ local function write_file(name, text, dont_dedent) file:close() end --- Tries to get platform name from $SYSTEM_NAME, uname; fallback is "Windows". -local uname = (function() - local platform = nil - return (function() - if platform then - return platform - end - - platform = os.getenv("SYSTEM_NAME") - if platform then - return platform - end - - local status, f = pcall(io.popen, "uname -s") - if status then - platform = f:read("*l") - else - platform = 'Windows' - end - return platform - 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 function source(code) local fname = tmpname() write_file(fname, code) @@ -475,6 +441,12 @@ end local function rmdir(path) local ret, _ = pcall(do_rmdir, path) + if not ret and os_name() == "windows" then + -- Maybe "Permission denied"; try again after changing the nvim + -- process to the top-level directory. + nvim_command([[exe 'cd '.fnameescape(']]..start_dir.."')") + ret, _ = pcall(do_rmdir, path) + end -- During teardown, the nvim process may not exit quickly enough, then rmdir() -- will fail (on Windows). if not ret then -- Try again. @@ -520,12 +492,12 @@ local function create_callindex(func) end -- Helper to skip tests. Returns true in Windows systems. --- pending_func is pending() from busted -local function pending_win32(pending_func) +-- pending_fn is pending() from busted +local function pending_win32(pending_fn) clear() if uname() == 'Windows' then - if pending_func ~= nil then - pending_func('FIXME: Windows', function() end) + if pending_fn ~= nil then + pending_fn('FIXME: Windows', function() end) end return true else @@ -533,6 +505,22 @@ local function pending_win32(pending_func) end end +-- Calls pending() and returns `true` if the system is too slow to +-- run fragile or expensive tests. Else returns `false`. +local function skip_fragile(pending_fn, cond) + if pending_fn == nil or type(pending_fn) ~= type(function()end) then + error("invalid pending_fn") + end + if cond then + pending_fn("skipped (test is fragile on this system)", function() end) + return true + elseif os.getenv("TEST_SKIP_FRAGILE") then + pending_fn("skipped (TEST_SKIP_FRAGILE)", function() end) + return true + end + return false +end + local funcs = create_callindex(nvim_call) local meths = create_callindex(nvim) local uimeths = create_callindex(ui) @@ -601,6 +589,7 @@ return function(after_each) curwinmeths = curwinmeths, curtabmeths = curtabmeths, pending_win32 = pending_win32, + skip_fragile = skip_fragile, tmpname = tmpname, NIL = mpack.NIL, } diff --git a/test/functional/legacy/autochdir_spec.lua b/test/functional/legacy/autochdir_spec.lua new file mode 100644 index 0000000000..06f7c1dd11 --- /dev/null +++ b/test/functional/legacy/autochdir_spec.lua @@ -0,0 +1,26 @@ +local lfs = require('lfs') +local helpers = require('test.functional.helpers')(after_each) +local clear, eq = helpers.clear, helpers.eq +local eval, execute = helpers.eval, helpers.execute + +describe('autochdir behavior', function() + local dir = 'Xtest-functional-legacy-autochdir' + + before_each(function() + lfs.mkdir(dir) + clear() + end) + + after_each(function() + helpers.rmdir(dir) + end) + + -- Tests vim/vim/777 without test_autochdir(). + it('sets filename', function() + execute('set acd') + execute('new') + execute('w '..dir..'/Xtest') + eq('Xtest', eval("expand('%')")) + eq(dir, eval([[substitute(getcwd(), '.*[/\\]\(\k*\)', '\1', '')]])) + end) +end) diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index 1ae855f26c..caeca5e4e2 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -9,8 +9,6 @@ local eval = helpers.eval local eq = helpers.eq local neq = helpers.neq -if helpers.pending_win32(pending) then return end - local function init_session(...) local args = { helpers.nvim_prog, '-i', 'NONE', '--embed', '--cmd', 'set shortmess+=I background=light noswapfile noautoindent', @@ -24,6 +22,8 @@ end describe('startup defaults', function() describe(':filetype', function() + if helpers.pending_win32(pending) then return end + local function expect_filetype(expected) local screen = Screen.new(48, 4) screen:attach() @@ -99,8 +99,37 @@ describe('startup defaults', function() end) describe('XDG-based defaults', function() - -- Need to be in separate describe() block to not run clear() twice. + -- Need separate describe() blocks to not run clear() twice. -- Do not put before_each() here for the same reasons. + + describe('with empty/broken environment', function() + it('sets correct defaults', function() + clear({env={ + XDG_CONFIG_HOME=nil, + XDG_DATA_HOME=nil, + XDG_CACHE_HOME=nil, + XDG_RUNTIME_DIR=nil, + XDG_CONFIG_DIRS=nil, + XDG_DATA_DIRS=nil, + LOCALAPPDATA=nil, + HOMEPATH=nil, + HOMEDRIVE=nil, + HOME=nil, + TEMP=nil, + VIMRUNTIME=nil, + USER=nil, + }}) + + eq('.', meths.get_option('backupdir')) + eq('.', meths.get_option('viewdir')) + eq('.', meths.get_option('directory')) + eq('.', meths.get_option('undodir')) + end) + end) + + -- TODO(jkeyes): tests below fail on win32 because of path separator. + if helpers.pending_win32(pending) then return end + describe('with too long XDG variables', function() before_each(function() clear({env={ diff --git a/test/functional/plugin/msgpack_spec.lua b/test/functional/plugin/msgpack_spec.lua index c8da8e8f6c..5ba19708cf 100644 --- a/test/functional/plugin/msgpack_spec.lua +++ b/test/functional/plugin/msgpack_spec.lua @@ -652,6 +652,8 @@ describe('In autoload/msgpack.vim', function() eval_eq('integer', ('a'):byte(), '\'a\'') eval_eq('integer', 0xAB, '\'«\'') + eval_eq('integer', 0, '\'\\0\'') + eval_eq('integer', 10246567, '\'\\10246567\'') end) it('correctly loads constants', function() diff --git a/test/functional/plugin/shada_spec.lua b/test/functional/plugin/shada_spec.lua index b1209a22e9..b543037ae2 100644 --- a/test/functional/plugin/shada_spec.lua +++ b/test/functional/plugin/shada_spec.lua @@ -609,6 +609,18 @@ describe('In autoload/shada.vim', function() 'abc', -1, ]}] ]]):gsub('\n', '')) + -- Regression: NUL separator must be properly supported + sd2strings_eq({ + 'History entry with timestamp ' .. epoch .. ':', + ' @ Description_ Value', + ' - history type SEARCH', + ' - contents ""', + ' - separator \'\\0\'', + }, ([[ [{'type': 4, 'timestamp': 0, 'data': [ + 1, + '', + 0x0 + ]}] ]]):gsub('\n', '')) end) it('works with register items', function() @@ -837,7 +849,7 @@ describe('In autoload/shada.vim', function() sd2strings_eq({ 'Global mark with timestamp ' .. epoch .. ':', ' % Key Description Value', - ' + n name 20', + ' + n name \'\\20\'', ' + f file name "foo"', ' # Value is negative', ' + l line number -10', @@ -852,7 +864,18 @@ describe('In autoload/shada.vim', function() sd2strings_eq({ 'Global mark with timestamp ' .. epoch .. ':', ' % Key Description Value', - ' + n name 20', + ' + n name 128', + ' + f file name "foo"', + ' + l line number 1', + ' + c column 0', + }, ([[ [{'type': 7, 'timestamp': 0, 'data': { + 'n': 128, + 'f': 'foo', + }}] ]]):gsub('\n', '')) + sd2strings_eq({ + 'Global mark with timestamp ' .. epoch .. ':', + ' % Key Description Value', + ' + n name \'\\20\'', ' + f file name "foo"', ' # Expected integer', ' + l line number "FOO"', @@ -1123,7 +1146,7 @@ describe('In autoload/shada.vim', function() 'Local mark with timestamp ' .. epoch .. ':', ' % Key Description Value', ' + f file name "foo"', - ' + n name 20', + ' + n name \'\\20\'', ' # Value is negative', ' + l line number -10', ' # Value is negative', @@ -1138,7 +1161,7 @@ describe('In autoload/shada.vim', function() 'Local mark with timestamp ' .. epoch .. ':', ' % Key Description Value', ' + f file name "foo"', - ' + n name 20', + ' + n name \'\\20\'', ' # Expected integer', ' + l line number "FOO"', ' # Expected integer', @@ -1932,13 +1955,13 @@ describe('In autoload/shada.vim', function() 'Buffer list with timestamp ' .. epoch .. ':', ' % Key Description Value', ' # Expected binary string', - ' + f file name 10', + ' + f file name \'\\10\'', ' + l line number 1', ' + c column 0', '', ' % Key Description Value', ' # Expected binary string', - ' + f file name 20', + ' + f file name \'\\20\'', ' + l line number 1', ' + c column 0', }) @@ -1948,7 +1971,7 @@ describe('In autoload/shada.vim', function() 'Buffer list with timestamp ' .. epoch .. ':', ' % Key Description Value', ' # Expected binary string', - ' + f file name 10', + ' + f file name \'\\10\'', ' + l line number 1', ' + c column 0', '', diff --git a/test/functional/shada/buffers_spec.lua b/test/functional/shada/buffers_spec.lua index 7e6338897a..a4746c2205 100644 --- a/test/functional/shada/buffers_spec.lua +++ b/test/functional/shada/buffers_spec.lua @@ -8,8 +8,6 @@ local reset, set_additional_cmd, clear = shada_helpers.reset, shada_helpers.set_additional_cmd, shada_helpers.clear -if helpers.pending_win32(pending) then return end - describe('ShaDa support code', function() local testfilename = 'Xtestfile-functional-shada-buffers' local testfilename_2 = 'Xtestfile-functional-shada-buffers-2' diff --git a/test/functional/shada/marks_spec.lua b/test/functional/shada/marks_spec.lua index b7c0f61f57..fa760ceb5b 100644 --- a/test/functional/shada/marks_spec.lua +++ b/test/functional/shada/marks_spec.lua @@ -14,8 +14,6 @@ local nvim_current_line = function() return curwinmeths.get_cursor()[1] end -if helpers.pending_win32(pending) then return end - describe('ShaDa support code', function() local testfilename = 'Xtestfile-functional-shada-marks' local testfilename_2 = 'Xtestfile-functional-shada-marks-2' @@ -153,6 +151,19 @@ describe('ShaDa support code', function() eq(saved, redir_exec('jumps')) end) + it('when dumping jump list also dumps current position', function() + nvim_command('edit ' .. testfilename) + nvim_command('normal! G') + nvim_command('split ' .. testfilename_2) + nvim_command('normal! G') + nvim_command('wshada') + nvim_command('quit') + nvim_command('rshada') + nvim_command('normal! \15') -- <C-o> + eq(testfilename_2, funcs.bufname('%')) + eq({2, 0}, curwinmeths.get_cursor()) + end) + it('is able to dump and restore jump list with different times (slow!)', function() nvim_command('edit ' .. testfilename_2) diff --git a/test/functional/shada/shada_spec.lua b/test/functional/shada/shada_spec.lua index f845f6f93b..32598fc399 100644 --- a/test/functional/shada/shada_spec.lua +++ b/test/functional/shada/shada_spec.lua @@ -23,8 +23,6 @@ local wshada, _, shada_fname, clean = local dirname = 'Xtest-functional-shada-shada.d' local dirshada = dirname .. '/main.shada' -if helpers.pending_win32(pending) then return end - describe('ShaDa support code', function() before_each(reset) after_each(function() @@ -173,6 +171,7 @@ describe('ShaDa support code', function() end it('correctly uses shada-r option', function() + nvim_command('set shellslash') meths.set_var('__home', paths.test_source_path) nvim_command('let $HOME = __home') nvim_command('unlet __home') @@ -196,6 +195,7 @@ describe('ShaDa support code', function() end) it('correctly ignores case with shada-r option', function() + nvim_command('set shellslash') local pwd = funcs.getcwd() local relfname = 'абв/test' local fname = pwd .. '/' .. relfname @@ -240,6 +240,8 @@ describe('ShaDa support code', function() end) it('does not crash when ShaDa file directory is not writable', function() + if helpers.pending_win32(pending) then return end + funcs.mkdir(dirname, '', 0) eq(0, funcs.filewritable(dirname)) set_additional_cmd('set shada=') diff --git a/test/functional/ui/mouse_spec.lua b/test/functional/ui/mouse_spec.lua index b729b0db08..17d949825a 100644 --- a/test/functional/ui/mouse_spec.lua +++ b/test/functional/ui/mouse_spec.lua @@ -153,9 +153,10 @@ describe('Mouse input', function() end) it('in tabline to the left moves tab left', function() - if os.getenv("TRAVIS") and (helpers.os_name() == "osx" - or os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN") then - pending("[Fails on Travis macOS, ASAN_UBSAN. #4874]", function() end) + if helpers.skip_fragile(pending, + os.getenv("TRAVIS") and (helpers.os_name() == "osx" + or os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN")) -- #4874 + then return end @@ -257,9 +258,10 @@ describe('Mouse input', function() end) it('out of tabline to the left moves tab left', function() - if os.getenv("TRAVIS") and (helpers.os_name() == "osx" - or os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN") then - pending("[Fails on Travis macOS, ASAN_UBSAN. #4874]", function() end) + if helpers.skip_fragile(pending, + os.getenv("TRAVIS") and (helpers.os_name() == "osx" + or os.getenv("CLANG_SANITIZER") == "ASAN_UBSAN")) -- #4874 + then return end diff --git a/test/helpers.lua b/test/helpers.lua index 4c50c7644f..0bc62da5d7 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -52,9 +52,49 @@ local function check_logs() assert(0 == runtime_errors) end +-- Tries to get platform name from $SYSTEM_NAME, uname; fallback is "Windows". +local uname = (function() + local platform = nil + return (function() + if platform then + return platform + end + + platform = os.getenv("SYSTEM_NAME") + if platform then + return platform + end + + local status, f = pcall(io.popen, "uname -s") + if status then + platform = f:read("*l") + else + platform = 'Windows' + end + return platform + 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 + return { eq = eq, neq = neq, ok = ok, check_logs = check_logs, + uname = uname, + tmpname = tmpname, } diff --git a/test/unit/api/private_helpers_spec.lua b/test/unit/api/private_helpers_spec.lua index 1d7c03787b..8c54ea6a2a 100644 --- a/test/unit/api/private_helpers_spec.lua +++ b/test/unit/api/private_helpers_spec.lua @@ -7,6 +7,7 @@ local NULL = helpers.NULL local eq = helpers.eq local lua2typvalt = eval_helpers.lua2typvalt +local typvalt2lua = eval_helpers.typvalt2lua local typvalt = eval_helpers.typvalt local nil_value = api_helpers.nil_value @@ -14,6 +15,7 @@ local list_type = api_helpers.list_type local int_type = api_helpers.int_type local type_key = api_helpers.type_key local obj2lua = api_helpers.obj2lua +local func_type = api_helpers.func_type local api = cimport('./src/nvim/api/private/helpers.h') @@ -85,4 +87,19 @@ describe('vim_to_object', function() eq(nil, tt.vval.v_dict) eq({}, obj2lua(api.vim_to_object(tt))) end) + + it('regression: partials in a list', function() + local llist = { + { + [type_key]=func_type, + value='printf', + args={'%s'}, + dict={v=1}, + }, + {}, + } + local list = lua2typvalt(llist) + eq(llist, typvalt2lua(list)) + eq({nil_value, {}}, obj2lua(api.vim_to_object(list))) + end) end) diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index 45fbf8da5c..c3c27e4fed 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -5,10 +5,12 @@ 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_defs.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 list_type = {[true]='list type'} local dict_type = {[true]='dict type'} @@ -18,27 +20,28 @@ local flt_type = {[true]='flt type'} local nil_value = {[true]='nil'} +local lua2typvalt + +local function li_alloc(nogc) + local gcfunc = eval.listitem_free + if nogc then gcfunc = nil end + local li = ffi.gc(eval.listitem_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 typ = type(val) - if typ == 'string' then - eval.list_append_string(ret, to_cstr(val)) - elseif typ == 'table' and val == null_string then - eval.list_append_string(ret, nil) - elseif typ == 'table' and val == null_list then - eval.list_append_list(ret, nil) - elseif typ == 'table' and val[type_key] == list_type then - local itemlist = ffi.gc(list(table.unpack(val)), nil) - eq(1, itemlist.lv_refcount) - itemlist.lv_refcount = 0 - eval.list_append_list(ret, itemlist) - else - assert(false, 'Not implemented yet') - end + local li_tv = ffi.gc(lua2typvalt(val), nil) + local li = li_alloc(true) + li.li_tv = li_tv + eval.tv_list_append(ret, li) end return ret end @@ -49,9 +52,14 @@ local special_tab = { [eval.kSpecialVarTrue] = true, } +local ptr2key = function(ptr) + return tostring(ptr) +end + local lst2tbl local dct2tbl +local typvalt2lua local typvalt2lua_tab typvalt2lua_tab = { @@ -72,33 +80,88 @@ typvalt2lua_tab = { return ffi.string(str) end end, - [tonumber(eval.VAR_LIST)] = function(t) - return lst2tbl(t.vval.v_list) + [tonumber(eval.VAR_LIST)] = function(t, processed) + return lst2tbl(t.vval.v_list, processed) end, - [tonumber(eval.VAR_DICT)] = function(t) - return dct2tbl(t.vval.v_dict) + [tonumber(eval.VAR_DICT)] = function(t, processed) + return dct2tbl(t.vval.v_dict, processed) end, - [tonumber(eval.VAR_FUNC)] = function(t) - return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t)} + [tonumber(eval.VAR_FUNC)] = function(t, processed) + return {[type_key]=func_type, value=typvalt2lua_tab[eval.VAR_STRING](t, processed or {})} + end, + [tonumber(eval.VAR_PARTIAL)] = function(t, processed) + local p_key = ptr2key(t) + 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, + } end, } -local typvalt2lua = function(t) +typvalt2lua = function(t, processed) return ((typvalt2lua_tab[tonumber(t.v_type)] or function(t_inner) assert(false, 'Converting ' .. tonumber(t_inner.v_type) .. ' was not implemented yet') - end)(t)) + end)(t, processed or {})) end -lst2tbl = function(l) - local ret = {[type_key]=list_type} +local function list_iter(l) + local init_s = { + idx=0, + li=l.lv_first, + } + local function f(s, _) + -- (listitem_T *) NULL is equal to nil, but yet it is not false. + if s.li == nil then + return nil + end + local ret_li = s.li + s.li = s.li.li_next + s.idx = s.idx + 1 + return s.idx, ret_li + end + return f, init_s, nil +end + +local function list_items(l) + local ret = {} + for i, li in list_iter(l) do + ret[i] = li + end + return ret +end + +lst2tbl = function(l, processed) if l == nil then - return ret + return null_list + end + processed = processed or {} + local p_key = ptr2key(l) + if processed[p_key] then + return processed[p_key] end - local li = l.lv_first - -- (listitem_T *) NULL is equal to nil, but yet it is not false. - while li ~= nil do - ret[#ret + 1] = typvalt2lua(li.li_tv) - li = li.li_next + local ret = {[type_key]=list_type} + processed[p_key] = ret + for i, li in list_iter(l) do + ret[i] = typvalt2lua(li.li_tv, processed) end if ret[1] then ret[type_key] = nil @@ -106,16 +169,71 @@ lst2tbl = function(l) return ret end -dct2tbl = function(d) - local ret = {d=d} - assert(false, 'Converting dictionaries is not implemented yet') +local hi_key_removed = eval._hash_key_removed() + +local function dict_iter(d, return_hi) + local init_s = { + todo=d.dv_hashtab.ht_used, + hi=d.dv_hashtab.ht_array, + } + local function f(s, _) + if s.todo == 0 then return nil end + while s.todo > 0 do + if s.hi.hi_key ~= nil and s.hi.hi_key ~= hi_key_removed then + local key = ffi.string(s.hi.hi_key) + local ret + if return_hi then + ret = s.hi + else + ret = ffi.cast('dictitem_T*', + s.hi.hi_key - ffi.offsetof('dictitem_T', 'di_key')) + end + s.todo = s.todo - 1 + s.hi = s.hi + 1 + return key, ret + end + s.hi = s.hi + 1 + end + end + return f, init_s, nil +end + +local function first_di(d) + local f, init_s, v = dict_iter(d) + return select(2, f(init_s, v)) +end + +local function dict_items(d) + local ret = {[0]=0} + for k, hi in dict_iter(d) do + ret[k] = hi + ret[0] = ret[0] + 1 + ret[ret[0]] = hi + end return ret end -local lua2typvalt +dct2tbl = function(d, processed) + if d == nil then + return null_dict + end + processed = processed or {} + local p_key = ptr2key(d) + if processed[p_key] then + return processed[p_key] + end + local ret = {} + processed[p_key] = ret + for k, di in dict_iter(d) do + ret[k] = typvalt2lua(di.di_tv, processed) + end + return ret +end local typvalt = function(typ, vval) - if type(typ) == 'string' then + if typ == nil then + typ = eval.VAR_UNKNOWN + elseif type(typ) == 'string' then typ = eval[typ] end return ffi.gc(ffi.new('typval_T', {v_type=typ, vval=vval}), eval.clear_tv) @@ -164,12 +282,68 @@ local lua2typvalt_type_tab = { end return ret end, + [func_type] = function(l, processed) + if processed[l] then + processed[l].pt_refcount = processed[l].pt_refcount + 1 + 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 + return typvalt(eval.VAR_PARTIAL, {v_partial=pt}) + else + return typvalt(eval.VAR_FUNC, { + v_string=eval.xmemdupz(to_cstr(l.value), #l.value) + }) + end + end, } +local special_vals = { + [null_string] = {eval.VAR_STRING, {v_string=ffi.cast('char_u*', nil)}}, + [null_list] = {eval.VAR_LIST, {v_list=ffi.cast('list_T*', nil)}}, + [null_dict] = {eval.VAR_DICT, {v_dict=ffi.cast('dict_T*', nil)}}, + [nil_value] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarNull}}, + [true] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarTrue}}, + [false] = {eval.VAR_SPECIAL, {v_special=eval.kSpecialVarFalse}}, +} + +for k, v in pairs(special_vals) do + local tmp = function(typ, vval) + special_vals[k] = function() + return typvalt(typ, vval) + end + end + tmp(v[1], v[2]) +end + lua2typvalt = function(l, processed) processed = processed or {} if l == nil or l == nil_value then - return typvalt(eval.VAR_SPECIAL, {v_special=eval.kSpecialVarNull}) + return special_vals[nil_value]() + elseif special_vals[l] then + return special_vals[l]() elseif type(l) == 'table' then if l[type_key] then return lua2typvalt_type_tab[l[type_key]](l, processed) @@ -182,18 +356,35 @@ lua2typvalt = function(l, processed) end elseif type(l) == 'number' then return typvalt(eval.VAR_FLOAT, {v_float=l}) - elseif type(l) == 'boolean' then - return typvalt(eval.VAR_SPECIAL, { - v_special=(l and eval.kSpecialVarTrue or eval.kSpecialVarFalse) - }) elseif type(l) == 'string' then 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.tv_copy(l, tv) + return tv end end +local function void(ptr) + return ffi.cast('void*', ptr) +end + +local alloc_logging_helpers = { + list = function(l) return {func='calloc', args={1, ffi.sizeof('list_T')}, ret=void(l)} end, + 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) + 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, + + freed = function(p) return {func='free', args={p and void(p)}} end, +} + return { null_string=null_string, null_list=null_list, + null_dict=null_dict, list_type=list_type, dict_type=dict_type, func_type=func_type, @@ -212,4 +403,15 @@ return { typvalt2lua=typvalt2lua, typvalt=typvalt, + + li_alloc=li_alloc, + + dict_iter=dict_iter, + list_iter=list_iter, + first_di=first_di, + + alloc_logging_helpers=alloc_logging_helpers, + + list_items=list_items, + dict_items=dict_items, } diff --git a/test/unit/eval/tv_clear_spec.lua b/test/unit/eval/tv_clear_spec.lua new file mode 100644 index 0000000000..96eccdbd71 --- /dev/null +++ b/test/unit/eval/tv_clear_spec.lua @@ -0,0 +1,127 @@ +local helpers = require('test.unit.helpers') +local eval_helpers = require('test.unit.eval.helpers') + +local alloc_log_new = helpers.alloc_log_new +local cimport = helpers.cimport +local ffi = helpers.ffi +local eq = helpers.eq + +local a = eval_helpers.alloc_logging_helpers +local type_key = eval_helpers.type_key +local list_type = eval_helpers.list_type +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 alloc_log = alloc_log_new() + +before_each(function() + alloc_log:before_each() +end) + +after_each(function() + alloc_log:after_each() +end) + +describe('clear_tv()', function() + it('successfully frees all lists in [&l [1], *l, *l]', function() + local l_inner = {1} + local list = {l_inner, l_inner, l_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local list_inner_p = lis[1].li_tv.vval.v_list + local lis_inner = list_items(list_inner_p) + alloc_log:check({ + a.list(list_p), + a.list(list_inner_p), + a.li(lis_inner[1]), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + }) + eq(3, list_inner_p.lv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(lis_inner[1]), + a.freed(list_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(lis[3]), + a.freed(list_p), + }) + end) + it('successfully frees all lists in [&l [], *l, *l]', function() + local l_inner = {[type_key]=list_type} + local list = {l_inner, l_inner, l_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local list_inner_p = lis[1].li_tv.vval.v_list + alloc_log:check({ + a.list(list_p), + a.list(list_inner_p), + a.li(lis[1]), + a.li(lis[2]), + a.li(lis[3]), + }) + eq(3, list_inner_p.lv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(list_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(lis[3]), + a.freed(list_p), + }) + end) + it('successfully frees all dictionaries in [&d {}, *d]', function() + local d_inner = {} + local list = {d_inner, d_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local dict_inner_p = lis[1].li_tv.vval.v_dict + alloc_log:check({ + a.list(list_p), + a.dict(dict_inner_p), + a.li(lis[1]), + a.li(lis[2]), + }) + eq(2, dict_inner_p.dv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(dict_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(list_p), + }) + end) + it('successfully frees all dictionaries in [&d {a: 1}, *d]', function() + local d_inner = {a=1} + local list = {d_inner, d_inner} + local list_tv = ffi.gc(lua2typvalt(list), nil) + local list_p = list_tv.vval.v_list + local lis = list_items(list_p) + local dict_inner_p = lis[1].li_tv.vval.v_dict + local dis = dict_items(dict_inner_p) + alloc_log:check({ + a.list(list_p), + a.dict(dict_inner_p), + a.di(dis.a, 1), + a.li(lis[1]), + a.li(lis[2]), + }) + eq(2, dict_inner_p.dv_refcount) + lib.clear_tv(list_tv) + alloc_log:check({ + a.freed(dis.a), + a.freed(dict_inner_p), + a.freed(lis[1]), + a.freed(lis[2]), + a.freed(list_p), + }) + end) +end) diff --git a/test/unit/formatc.lua b/test/unit/formatc.lua index 00637e0b8d..e288081960 100644 --- a/test/unit/formatc.lua +++ b/test/unit/formatc.lua @@ -219,13 +219,7 @@ local function standalone(...) -- luacheck: ignore Preprocess.add_to_include_path('./../../build/include') Preprocess.add_to_include_path('./../../.deps/usr/include') - local input = Preprocess.preprocess_stream(arg[1]) - local raw = input:read('*all') - input:close() - - if raw == nil then - print("ERROR: Preprocess.preprocess_stream():read() returned empty") - end + local raw = Preprocess.preprocess('', arg[1]) local formatted if #arg == 2 and arg[2] == 'no' then diff --git a/test/unit/helpers.lua b/test/unit/helpers.lua index 3564f76442..1bfdd32739 100644 --- a/test/unit/helpers.lua +++ b/test/unit/helpers.lua @@ -9,6 +9,12 @@ local neq = global_helpers.neq local eq = global_helpers.eq local ok = global_helpers.ok +-- C constants. +local NULL = ffi.cast('void*', 0) + +local OK = 1 +local FAIL = 0 + -- add some standard header locations for _, p in ipairs(Paths.include_paths) do Preprocess.add_to_include_path(p) @@ -45,6 +51,8 @@ local function filter_complex_blocks(body) return table.concat(result, "\n") end +local previous_defines = '' + -- use this helper to import C files, you can pass multiple paths at once, -- this helper will return the C namespace of the nvim library. local function cimport(...) @@ -66,17 +74,8 @@ local function cimport(...) return libnvim end - local body = nil - for _ = 1, 10 do - local stream = Preprocess.preprocess_stream(unpack(paths)) - body = stream:read("*a") - stream:close() - if body ~= nil then break end - end - - if body == nil then - print("ERROR: helpers.lua: Preprocess.preprocess_stream():read() returned empty") - end + local body + body, previous_defines = Preprocess.preprocess(previous_defines, unpack(paths)) -- format it (so that the lines are "unique" statements), also filter out -- Objective-C blocks @@ -125,6 +124,67 @@ local function cppimport(path) return cimport(Paths.test_include_path .. '/' .. path) end +local function alloc_log_new() + local log = { + log={}, + lib=cimport('./src/nvim/memory.h'), + original_functions={}, + null={['\0:is_null']=true}, + } + local allocator_functions = {'malloc', 'free', 'calloc', 'realloc'} + function log:save_original_functions() + for _, funcname in ipairs(allocator_functions) do + self.original_functions[funcname] = self.lib['mem_' .. funcname] + end + end + function log:set_mocks() + for _, k in ipairs(allocator_functions) do + do + local kk = k + self.lib['mem_' .. k] = function(...) + local log_entry = {func=kk, args={...}} + self.log[#self.log + 1] = log_entry + if kk == 'free' then + self.original_functions[kk](...) + else + log_entry.ret = self.original_functions[kk](...) + end + for i, v in ipairs(log_entry.args) do + if v == nil then + -- XXX This thing thinks that {NULL} ~= {NULL}. + log_entry.args[i] = self.null + end + end + if self.hook then self:hook(log_entry) end + if log_entry.ret then + return log_entry.ret + end + end + end + end + end + function log:clear() + self.log = {} + end + function log:check(exp) + eq(exp, self.log) + self:clear() + end + function log:restore_original_functions() + for k, v in pairs(self.original_functions) do + self.lib['mem_' .. k] = v + end + end + function log:before_each() + log:save_original_functions() + log:set_mocks() + end + function log:after_each() + log:restore_original_functions() + end + return log +end + cimport('./src/nvim/types.h') -- take a pointer to a C-allocated string and return an interned @@ -136,7 +196,7 @@ end local cstr = ffi.typeof('char[?]') local function to_cstr(string) - return cstr((string.len(string)) + 1, string) + return cstr(#string + 1, string) end -- initialize some global variables, this is still necessary to unit test @@ -149,12 +209,6 @@ do main.event_init() end --- C constants. -local NULL = ffi.cast('void*', 0) - -local OK = 1 -local FAIL = 0 - return { cimport = cimport, cppimport = cppimport, @@ -168,5 +222,6 @@ return { to_cstr = to_cstr, NULL = NULL, OK = OK, - FAIL = FAIL + FAIL = FAIL, + alloc_log_new = alloc_log_new, } diff --git a/test/unit/path_spec.lua b/test/unit/path_spec.lua index 9b76834383..ccaf0228ab 100644 --- a/test/unit/path_spec.lua +++ b/test/unit/path_spec.lua @@ -336,6 +336,17 @@ describe('more path function', function() eq(FAIL, result) end) + it('fails safely if given length is wrong #5737', function() + local force_expansion = 1 + local filename = 'foo/bar/bazzzzzzz/buz/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/a' + local too_short_len = 8 + local buf = cstr(too_short_len, '') + local result = path.vim_FullName(filename, buf, too_short_len, force_expansion) + local expected = string.sub(filename, 1, (too_short_len - 1)) + eq(expected, (ffi.string(buf))) + eq(FAIL, result) + end) + it('uses the filename if the filename is a URL', function() local force_expansion = 1 local filename = 'http://www.neovim.org' diff --git a/test/unit/preprocess.lua b/test/unit/preprocess.lua index 10ba997758..1c9b290462 100644 --- a/test/unit/preprocess.lua +++ b/test/unit/preprocess.lua @@ -7,22 +7,22 @@ local ccs = {} local env_cc = os.getenv("CC") if env_cc then - table.insert(ccs, {path = "/usr/bin/env " .. tostring(env_cc), type = "gcc"}) + table.insert(ccs, {path = {"/usr/bin/env", env_cc}, type = "gcc"}) end if ffi.os == "Windows" then - table.insert(ccs, {path = "cl", type = "msvc"}) + table.insert(ccs, {path = {"cl"}, type = "msvc"}) end -table.insert(ccs, {path = "/usr/bin/env cc", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.9", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.8", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env gcc-4.7", type = "gcc"}) -table.insert(ccs, {path = "/usr/bin/env clang", type = "clang"}) -table.insert(ccs, {path = "/usr/bin/env icc", type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "cc"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.9"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.8"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "gcc-4.7"}, type = "gcc"}) +table.insert(ccs, {path = {"/usr/bin/env", "clang"}, type = "clang"}) +table.insert(ccs, {path = {"/usr/bin/env", "icc"}, type = "gcc"}) -local quote_me = '[^%w%+%-%=%@%_%/]' -- complement (needn't quote) +local quote_me = '[^.%w%+%-%@%_%/]' -- complement (needn't quote) local function shell_quote(str) if string.find(str, quote_me) or str == '' then return "'" .. string.gsub(str, "'", [['"'"']]) .. "'" @@ -61,12 +61,12 @@ end -- will produce a string that represents a meta C header file that includes -- all the passed in headers. I.e.: -- --- headerize({"stdio.h", "math.h", true} +-- headerize({"stdio.h", "math.h"}, true) -- produces: -- #include <stdio.h> -- #include <math.h> -- --- headerize({"vim.h", "memory.h", false} +-- headerize({"vim.h", "memory.h"}, false) -- produces: -- #include "vim.h" -- #include "memory.h" @@ -79,8 +79,7 @@ local function headerize(headers, global) end local formatted = {} - for i = 1, #headers do - local hdr = headers[i] + for _, hdr in ipairs(headers) do formatted[#formatted + 1] = "#include " .. tostring(pre) .. tostring(hdr) .. @@ -91,49 +90,78 @@ local function headerize(headers, global) end local Gcc = { + preprocessor_extra_flags = {}, + get_defines_extra_flags = {'-std=c99', '-dM', '-E'}, + get_declarations_extra_flags = {'-std=c99', '-P', '-E'}, +} + +function Gcc:define(name, args, val) + local define = '-D' .. name + if args ~= nil then + define = define .. '(' .. table.concat(args, ',') .. ')' + end + if val ~= nil then + define = define .. '=' .. val + end + self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = define +end + +function Gcc:undefine(name) + self.preprocessor_extra_flags[#self.preprocessor_extra_flags + 1] = ( + '-U' .. name) +end + +function Gcc:init_defines() -- preprocessor flags that will hopefully make the compiler produce C -- declarations that the LuaJIT ffi understands. - preprocessor_extra_flags = { - '-D "aligned(ARGS)="', - '-D "__attribute__(ARGS)="', - '-D "__asm(ARGS)="', - '-D "__asm__(ARGS)="', - '-D "__inline__="', - '-D "EXTERN=extern"', - '-D "INIT(...)="', - '-D_GNU_SOURCE', - '-DINCLUDE_GENERATED_DECLARATIONS', - - -- Needed for FreeBSD - '-D "_Thread_local="', - - -- Needed for macOS Sierra - '-D "_Nullable="', - '-D "_Nonnull="', - '-U__BLOCKS__', - } -} + self:define('aligned', {'ARGS'}, '') + self:define('__attribute__', {'ARGS'}, '') + self:define('__asm', {'ARGS'}, '') + self:define('__asm__', {'ARGS'}, '') + self:define('__inline__', nil, '') + self:define('EXTERN', nil, 'extern') + self:define('INIT', {'...'}, '') + self:define('_GNU_SOURCE') + self:define('INCLUDE_GENERATED_DECLARATIONS') + self:define('UNIT_TESTING') + -- Needed for FreeBSD + self:define('_Thread_local', nil, '') + -- Needed for macOS Sierra + self:define('_Nullable', nil, '') + self:define('_Nonnull', nil, '') + self:undefine('__BLOCKS__') +end function Gcc:new(obj) obj = obj or {} setmetatable(obj, self) self.__index = self + self:init_defines() return obj end function Gcc:add_to_include_path(...) - local paths = {...} - for i = 1, #paths do - local path = paths[i] - local directive = '-I ' .. '"' .. path .. '"' + for i = 1, select('#', ...) do + local path = select(i, ...) local ef = self.preprocessor_extra_flags - ef[#ef + 1] = directive + ef[#ef + 1] = '-I' .. path + end +end + +local function argss_to_cmd(...) + local cmd = '' + for i = 1, select('#', ...) do + for _, arg in ipairs(select(i, ...)) do + cmd = cmd .. ' ' .. shell_quote(arg) + end end + return cmd end -- returns a list of the headers files upon which this file relies function Gcc:dependencies(hdr) - local out = io.popen(tostring(self.path) .. " -M " .. tostring(hdr) .. " 2>&1") + local cmd = argss_to_cmd(self.path, {'-M', hdr}) .. ' 2>&1' + local out = io.popen(cmd) local deps = out:read("*a") out:close() if deps then @@ -143,23 +171,51 @@ function Gcc:dependencies(hdr) end end +local function repeated_call(...) + local cmd = argss_to_cmd(...) + for _ = 1, 10 do + local stream = io.popen(cmd) + local ret = stream:read('*a') + stream:close() + if ret then + return ret + end + end + print('ERROR: preprocess.lua: Failed to execute ' .. cmd .. ': nil return after 10 attempts') + return nil +end + -- returns a stream representing a preprocessed form of the passed-in headers. -- Don't forget to close the stream by calling the close() method on it. -function Gcc:preprocess_stream(...) +function Gcc:preprocess(previous_defines, ...) -- create pseudo-header local pseudoheader = headerize({...}, false) - local defines = table.concat(self.preprocessor_extra_flags, ' ') - local cmd = ("echo $hdr | " .. - tostring(self.path) .. - " " .. - tostring(defines) .. - " -std=c99 -P -E -"):gsub('$hdr', shell_quote(pseudoheader)) + local pseudoheader_fname = 'tmp_pseudoheader.h' + local pseudoheader_file = io.open(pseudoheader_fname, 'w') + pseudoheader_file:write(previous_defines) + pseudoheader_file:write("\n") + pseudoheader_file:write(pseudoheader) + pseudoheader_file:flush() + pseudoheader_file:close() + + local defines = repeated_call(self.path, self.preprocessor_extra_flags, + self.get_defines_extra_flags, + {pseudoheader_fname}) + -- lfs = require("lfs") -- print("CWD: #{lfs.currentdir!}") -- print("CMD: #{cmd}") -- io.stderr\write("CWD: #{lfs.currentdir!}\n") -- io.stderr\write("CMD: #{cmd}\n") - return io.popen(cmd) + + local declarations = repeated_call(self.path, self.preprocessor_extra_flags, + self.get_declarations_extra_flags, + {pseudoheader_fname}) + + os.remove(pseudoheader_fname) + + assert(declarations and defines) + return declarations, defines end local Clang = Gcc:new() @@ -197,8 +253,8 @@ return { includes = function(hdr) return cc:dependencies(hdr) end, - preprocess_stream = function(...) - return cc:preprocess_stream(...) + preprocess = function(...) + return cc:preprocess(...) end, add_to_include_path = function(...) return cc:add_to_include_path(...) |