diff options
Diffstat (limited to 'test')
40 files changed, 1512 insertions, 394 deletions
diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index 3245e1b52d..aa91cd1396 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -8,8 +8,6 @@ local nvim_prog, command, funcs = helpers.nvim_prog, helpers.command, helpers.fu local source, next_message = helpers.source, helpers.next_message local meths = helpers.meths -if helpers.pending_win32(pending) then return end - describe('server -> client', function() local cid @@ -212,6 +210,8 @@ describe('server -> client', function() funcs.jobstop(jobid) end) + if helpers.pending_win32(pending) then return end + it('rpc and text stderr can be combined', function() eq("ok",funcs.rpcrequest(jobid, "poll")) funcs.rpcnotify(jobid, "ping") diff --git a/test/functional/autocmd/autocmd_spec.lua b/test/functional/autocmd/autocmd_spec.lua index 162e112047..c38bd75c69 100644 --- a/test/functional/autocmd/autocmd_spec.lua +++ b/test/functional/autocmd/autocmd_spec.lua @@ -12,8 +12,8 @@ describe('autocmds:', function() local expected = { 'WinLeave', 'TabLeave', - 'TabNew', 'WinEnter', + 'TabNew', 'TabEnter', 'BufLeave', 'BufEnter' diff --git a/test/functional/autocmd/bufenter_spec.lua b/test/functional/autocmd/bufenter_spec.lua new file mode 100644 index 0000000000..ccbcdf5c5e --- /dev/null +++ b/test/functional/autocmd/bufenter_spec.lua @@ -0,0 +1,35 @@ +local helpers = require('test.functional.helpers')(after_each) + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local eval = helpers.eval +local execute = helpers.execute +local request = helpers.request +local source = helpers.source + +describe('autocmd BufEnter', function() + before_each(clear) + + it("triggered by nvim_command('edit <dir>')", function() + command("autocmd BufEnter * if isdirectory(expand('<afile>')) | let g:dir_bufenter = 1 | endif") + request("nvim_command", "split .") + eq(1, eval("exists('g:dir_bufenter')")) -- Did BufEnter for the directory. + eq(2, eval("bufnr('%')")) -- Switched to the dir buffer. + end) + + it('triggered by "try|:split <dir>|endtry" in a function', function() + command("autocmd BufEnter * if isdirectory(expand('<afile>')) | let g:dir_bufenter = 1 | endif") + source([[ + function! Test() + try + exe 'split .' + catch + endtry + endfunction + ]]) + execute("call Test()") + eq(1, eval("exists('g:dir_bufenter')")) -- Did BufEnter for the directory. + eq(2, eval("bufnr('%')")) -- Switched to the dir buffer. + end) +end) diff --git a/test/functional/autocmd/dirchanged_spec.lua b/test/functional/autocmd/dirchanged_spec.lua new file mode 100644 index 0000000000..15196dbd44 --- /dev/null +++ b/test/functional/autocmd/dirchanged_spec.lua @@ -0,0 +1,113 @@ +local lfs = require('lfs') +local h = require('test.functional.helpers')(after_each) + +local clear = h.clear +local command = h.command +local eq = h.eq +local eval = h.eval +local request = h.request + +describe('autocmd DirChanged', function() + local curdir = string.gsub(lfs.currentdir(), '\\', '/') + local dirs = { + curdir .. '/Xtest-functional-autocmd-dirchanged.dir1', + curdir .. '/Xtest-functional-autocmd-dirchanged.dir2', + curdir .. '/Xtest-functional-autocmd-dirchanged.dir3', + } + + setup(function() for _, dir in pairs(dirs) do h.mkdir(dir) end end) + teardown(function() for _, dir in pairs(dirs) do h.rmdir(dir) end end) + + before_each(function() + clear() + command('autocmd DirChanged * let [g:event, g:scope, g:cdcount] = [copy(v:event), expand("<amatch>"), 1 + get(g:, "cdcount", 0)]') + end) + + it('sets v:event', function() + command('lcd '..dirs[1]) + eq({cwd=dirs[1], scope='window'}, eval('g:event')) + eq(1, eval('g:cdcount')) + + command('tcd '..dirs[2]) + eq({cwd=dirs[2], scope='tab'}, eval('g:event')) + eq(2, eval('g:cdcount')) + + command('cd '..dirs[3]) + eq({cwd=dirs[3], scope='global'}, eval('g:event')) + eq(3, eval('g:cdcount')) + end) + + it('disallows recursion', function() + command('set shellslash') + -- Set up a _nested_ handler. + command('autocmd DirChanged * nested lcd '..dirs[3]) + command('lcd '..dirs[1]) + eq({cwd=dirs[1], scope='window'}, eval('g:event')) + eq(1, eval('g:cdcount')) + -- autocmd changed to dirs[3], but did NOT trigger another DirChanged. + eq(dirs[3], eval('getcwd()')) + end) + + it('sets <amatch> to CWD "scope"', function() + command('lcd '..dirs[1]) + eq('window', eval('g:scope')) + + command('tcd '..dirs[2]) + eq('tab', eval('g:scope')) + + command('cd '..dirs[3]) + eq('global', eval('g:scope')) + end) + + it('does not trigger if :cd fails', function() + command('let g:event = {}') + + local status1, err1 = pcall(function() + command('lcd '..dirs[1] .. '/doesnotexist') + end) + eq({}, eval('g:event')) + + local status2, err2 = pcall(function() + command('lcd '..dirs[2] .. '/doesnotexist') + end) + eq({}, eval('g:event')) + + local status3, err3 = pcall(function() + command('lcd '..dirs[3] .. '/doesnotexist') + end) + eq({}, eval('g:event')) + + eq(false, status1) + eq(false, status2) + eq(false, status3) + + eq('E344:', string.match(err1, "E%d*:")) + eq('E344:', string.match(err2, "E%d*:")) + eq('E344:', string.match(err3, "E%d*:")) + end) + + it("is triggered by 'autochdir'", function() + command('set autochdir') + + command('split '..dirs[1]..'/foo') + eq({cwd=dirs[1], scope='window'}, eval('g:event')) + + command('split '..dirs[2]..'/bar') + eq({cwd=dirs[2], scope='window'}, eval('g:event')) + end) + + it('is triggered by nvim_set_current_dir()', function() + request('nvim_set_current_dir', dirs[1]) + eq({cwd=dirs[1], scope='global'}, eval('g:event')) + + request('nvim_set_current_dir', dirs[2]) + eq({cwd=dirs[2], scope='global'}, eval('g:event')) + + local status, err = pcall(function() + request('nvim_set_current_dir', '/doesnotexist') + end) + eq(false, status) + eq('Failed to change directory', string.match(err, ': (.*)')) + eq({cwd=dirs[2], scope='global'}, eval('g:event')) + end) +end) diff --git a/test/functional/autocmd/tabnew_spec.lua b/test/functional/autocmd/tabnew_spec.lua index 2148b21832..ad40954f76 100644 --- a/test/functional/autocmd/tabnew_spec.lua +++ b/test/functional/autocmd/tabnew_spec.lua @@ -5,8 +5,6 @@ local command = helpers.command local eq = helpers.eq local eval = helpers.eval -if helpers.pending_win32(pending) then return end - describe('autocmd TabNew', function() before_each(clear) @@ -19,12 +17,11 @@ describe('autocmd TabNew', function() end) it('matches when opening a new tab for FILE', function() - local tmp_path = helpers.funcs.tempname() command('let g:test = "foo"') - command('autocmd! TabNew ' .. tmp_path .. ' let g:test = "bar"') - command('tabnew ' .. tmp_path ..'X') + command('autocmd! TabNew Xtest-tabnew let g:test = "bar"') + command('tabnew Xtest-tabnewX') eq('foo', eval('g:test')) - command('tabnew ' .. tmp_path) + command('tabnew Xtest-tabnew') eq('bar', eval('g:test')) end) end) diff --git a/test/functional/autocmd/tabnewentered_spec.lua b/test/functional/autocmd/tabnewentered_spec.lua index f033bd5fe4..bdbe677132 100644 --- a/test/functional/autocmd/tabnewentered_spec.lua +++ b/test/functional/autocmd/tabnewentered_spec.lua @@ -1,8 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) local clear, nvim, eq = helpers.clear, helpers.nvim, helpers.eq -if helpers.pending_win32(pending) then return end - describe('TabNewEntered', function() describe('au TabNewEntered', function() describe('with * as <afile>', function() @@ -15,9 +13,9 @@ describe('TabNewEntered', function() end) describe('with FILE as <afile>', function() it('matches when opening a new tab for FILE', function() - local tmp_path = nvim('eval', 'tempname()') - nvim('command', 'au! TabNewEntered '..tmp_path..' echom "tabnewentered:match"') - eq("\n\""..tmp_path.."\" [New File]\ntabnewentered:4:4\ntabnewentered:match", nvim('command_output', 'tabnew '..tmp_path)) + nvim('command', 'au! TabNewEntered Xtest-tabnewentered echom "tabnewentered:match"') + eq('\n"Xtest-tabnewentered" [New File]\ntabnewentered:4:4\ntabnewentered:match', + nvim('command_output', 'tabnew Xtest-tabnewentered')) end) end) describe('with CTRL-W T', function() diff --git a/test/functional/core/job_partial_spec.lua b/test/functional/core/job_partial_spec.lua index b60f239db9..7643b283c4 100644 --- a/test/functional/core/job_partial_spec.lua +++ b/test/functional/core/job_partial_spec.lua @@ -2,13 +2,14 @@ local helpers = require('test.functional.helpers')(after_each) local clear, eq, next_msg, nvim, source = helpers.clear, helpers.eq, helpers.next_message, helpers.nvim, helpers.source -if helpers.pending_win32(pending) then return end - describe('jobs with partials', function() local channel before_each(function() clear() + if helpers.os_name() == 'windows' then + helpers.set_shell_powershell() + end channel = nvim('get_api_info')[1] nvim('set_var', 'channel', channel) end) @@ -16,12 +17,14 @@ describe('jobs with partials', function() it('works correctly', function() source([[ function PrintArgs(a1, a2, id, data, event) - call rpcnotify(g:channel, '1', a:a1, a:a2, a:data, a:event) + " Windows: Remove ^M char. + let normalized = map(a:data, 'substitute(v:val, "\r", "", "g")') + call rpcnotify(g:channel, '1', a:a1, a:a2, normalized, a:event) endfunction let Callback = function('PrintArgs', ["foo", "bar"]) let g:job_opts = {'on_stdout': Callback} - call jobstart(['echo'], g:job_opts) + call jobstart('echo "some text"', g:job_opts) ]]) - eq({'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}}, next_msg()) + eq({'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, next_msg()) end) end) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index 75b50aad0a..48a4689545 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -312,6 +312,24 @@ describe('jobs', function() end) end) + it('does not repeat output with slow output handlers', function() + source([[ + let d = {'data': []} + function! d.on_stdout(job, data, event) dict + call add(self.data, a:data) + sleep 200m + endfunction + if has('win32') + let cmd = '1,2,3,4,5 | foreach-object -process {echo $_; sleep 0.1}' + else + let cmd = ['sh', '-c', 'for i in $(seq 1 5); do echo $i; sleep 0.1; done'] + endif + call jobwait([jobstart(cmd, d)]) + call rpcnotify(g:channel, 'data', d.data) + ]]) + eq({'notification', 'data', {{{'1', ''}, {'2', ''}, {'3', ''}, {'4', ''}, {'5', ''}}}}, next_msg()) + end) + describe('jobwait', function() it('returns a list of status codes', function() source([[ 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/server_spec.lua b/test/functional/eval/server_spec.lua index d2c985e894..420aea04aa 100644 --- a/test/functional/eval/server_spec.lua +++ b/test/functional/eval/server_spec.lua @@ -4,8 +4,6 @@ local nvim, eq, neq, eval = helpers.nvim, helpers.eq, helpers.neq, helpers.eval local clear, funcs, meths = helpers.clear, helpers.funcs, helpers.meths local os_name = helpers.os_name -if helpers.pending_win32(pending) then return end - describe('serverstart(), serverstop()', function() before_each(clear) @@ -42,8 +40,8 @@ describe('serverstart(), serverstop()', function() -- v:servername will take the next available server. local servername = (os_name() == 'windows' - and [[\\.\pipe\Xtest-functional-server-server-pipe]] - or 'Xtest-functional-server-server-socket') + and [[\\.\pipe\Xtest-functional-server-pipe]] + or 'Xtest-functional-server-socket') funcs.serverstart(servername) eq(servername, meths.get_vvar('servername')) end) @@ -63,9 +61,11 @@ describe('serverlist()', function() local n = eval('len(serverlist())') -- Add a few - local servs = {'should-not-exist', 'another-one-that-shouldnt'} + local servs = (os_name() == 'windows' + and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] } + or { [[Xtest-pipe0934]], [[Xtest-pipe4324]] }) for _, s in ipairs(servs) do - eq(s, eval('serverstart("'..s..'")')) + eq(s, eval("serverstart('"..s.."')")) end local new_servs = eval('serverlist()') @@ -75,10 +75,9 @@ describe('serverlist()', function() -- The new servers should be at the end of the list. for i = 1, #servs do eq(servs[i], new_servs[i + n]) - nvim('command', 'call serverstop("'..servs[i]..'")') + nvim('command', "call serverstop('"..servs[i].."')") end - -- After calling serverstop() on the new servers, they should no longer be - -- in the list. + -- After serverstop() the servers should NOT be in the list. eq(n, eval('len(serverlist())')) end) end) diff --git a/test/functional/eval/setpos_spec.lua b/test/functional/eval/setpos_spec.lua new file mode 100644 index 0000000000..2e27cd8ac0 --- /dev/null +++ b/test/functional/eval/setpos_spec.lua @@ -0,0 +1,64 @@ +local helpers = require('test.functional.helpers')(after_each) +local setpos = helpers.funcs.setpos +local getpos = helpers.funcs.getpos +local insert = helpers.insert +local clear = helpers.clear +local execute = helpers.execute +local eval = helpers.eval +local eq = helpers.eq +local exc_exec = helpers.exc_exec + + +describe('setpos() function', function() + before_each(function() + clear() + insert([[ + First line of text + Second line of text + Third line of text]]) + execute('new') + insert([[ + Line of text 1 + Line of text 2 + Line of text 3]]) + end) + it('can set the current cursor position', function() + setpos(".", {0, 2, 1, 0}) + eq(getpos("."), {0, 2, 1, 0}) + setpos(".", {2, 1, 1, 0}) + eq(getpos("."), {0, 1, 1, 0}) + -- Ensure get an error attempting to set position to another buffer + local ret = exc_exec('call setpos(".", [1, 1, 1, 0])') + eq('Vim(call):E474: Invalid argument', ret) + end) + it('can set lowercase marks in the current buffer', function() + setpos("'d", {0, 2, 1, 0}) + eq(getpos("'d"), {0, 2, 1, 0}) + execute('undo', 'call setpos("\'d", [2, 3, 1, 0])') + eq(getpos("'d"), {0, 3, 1, 0}) + end) + it('can set lowercase marks in other buffers', function() + local retval = setpos("'d", {1, 2, 1, 0}) + eq(0, retval) + setpos("'d", {1, 2, 1, 0}) + eq(getpos("'d"), {0, 0, 0, 0}) + execute('wincmd w') + eq(eval('bufnr("%")'), 1) + eq(getpos("'d"), {0, 2, 1, 0}) + end) + it("fails when setting a mark in a buffer that doesn't exist", function() + local retval = setpos("'d", {3, 2, 1, 0}) + eq(-1, retval) + eq(getpos("'d"), {0, 0, 0, 0}) + retval = setpos("'D", {3, 2, 1, 0}) + eq(-1, retval) + eq(getpos("'D"), {0, 0, 0, 0}) + end) + it('can set uppercase marks', function() + setpos("'D", {2, 2, 3, 0}) + eq(getpos("'D"), {2, 2, 3, 0}) + -- Can set a mark in another buffer + setpos("'D", {1, 2, 2, 0}) + eq(getpos("'D"), {1, 2, 2, 0}) + end) +end) diff --git a/test/functional/eval/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/eval/system_spec.lua b/test/functional/eval/system_spec.lua index 6393477260..d5845132dd 100644 --- a/test/functional/eval/system_spec.lua +++ b/test/functional/eval/system_spec.lua @@ -1,12 +1,10 @@ local helpers = require('test.functional.helpers')(after_each) -local eq, clear, eval, execute, feed, nvim = - helpers.eq, helpers.clear, helpers.eval, helpers.execute, helpers.feed, - helpers.nvim +local eq, call, clear, eval, execute, feed, nvim = + helpers.eq, helpers.call, helpers.clear, helpers.eval, helpers.execute, + helpers.feed, helpers.nvim local Screen = require('test.functional.ui.screen') -if helpers.pending_win32(pending) then return end - local function create_file_with_nuls(name) return function() feed('ipart1<C-V>000part2<C-V>000part3<ESC>:w '..name..'<CR>') @@ -31,7 +29,70 @@ end describe('system()', function() before_each(clear) - it('sets the v:shell_error variable', function() + describe('command passed as a List', function() + local printargs_path = helpers.nvim_dir..'/printargs-test' + .. (helpers.os_name() == 'windows' and '.exe' or '') + + it('sets v:shell_error if cmd[0] is not executable', function() + call('system', { 'this-should-not-exist' }) + eq(-1, eval('v:shell_error')) + end) + + it('parameter validation does NOT modify v:shell_error', function() + -- 1. Call system() with invalid parameters. + -- 2. Assert that v:shell_error was NOT set. + execute('call system({})') + eq('E475: Invalid argument: expected String or List', eval('v:errmsg')) + eq(0, eval('v:shell_error')) + execute('call system([])') + eq('E474: Invalid argument', eval('v:errmsg')) + eq(0, eval('v:shell_error')) + + -- Provoke a non-zero v:shell_error. + call('system', { 'this-should-not-exist' }) + local old_val = eval('v:shell_error') + eq(-1, old_val) + + -- 1. Call system() with invalid parameters. + -- 2. Assert that v:shell_error was NOT modified. + execute('call system({})') + eq(old_val, eval('v:shell_error')) + execute('call system([])') + eq(old_val, eval('v:shell_error')) + end) + + it('quotes arguments correctly #5280', function() + local out = call('system', + { printargs_path, [[1]], [[2 "3]], [[4 ' 5]], [[6 ' 7']] }) + + eq(0, eval('v:shell_error')) + eq([[arg1=1;arg2=2 "3;arg3=4 ' 5;arg4=6 ' 7';]], out) + + out = call('system', { printargs_path, [['1]], [[2 "3]] }) + eq(0, eval('v:shell_error')) + eq([[arg1='1;arg2=2 "3;]], out) + + out = call('system', { printargs_path, "A\nB" }) + eq(0, eval('v:shell_error')) + eq("arg1=A\nB;", out) + end) + + it('calls executable in $PATH', function() + if 0 == eval("executable('python')") then pending("missing `python`") end + eq("foo\n", eval([[system(['python', '-c', 'print("foo")'])]])) + eq(0, eval('v:shell_error')) + end) + + it('does NOT run in shell', function() + if helpers.os_name() ~= 'windows' then + eq("* $PATH %PATH%\n", eval("system(['echo', '*', '$PATH', '%PATH%'])")) + end + end) + end) + + if helpers.pending_win32(pending) then return end + + it('sets v:shell_error', function() eval([[system("sh -c 'exit'")]]) eq(0, eval('v:shell_error')) eval([[system("sh -c 'exit 1'")]]) @@ -158,7 +219,7 @@ describe('system()', function() end) end) - describe('passing number as input', function() + describe('input passed as Number', function() it('stringifies the input', function() eq('1', eval('system("cat", 1)')) end) @@ -175,8 +236,8 @@ describe('system()', function() end) end) - describe('passing list as input', function() - it('joins list items with linefeed characters', function() + describe('input passed as List', function() + it('joins List items with linefeed characters', function() eq('line1\nline2\nline3', eval("system('cat -', ['line1', 'line2', 'line3'])")) end) @@ -185,7 +246,7 @@ describe('system()', function() -- is inconsistent and is a good reason for the existence of the -- `systemlist()` function, where input and output map to the same -- characters(see the following tests with `systemlist()` below) - describe('with linefeed characters inside list items', function() + describe('with linefeed characters inside List items', function() it('converts linefeed characters to NULs', function() eq('l1\001p2\nline2\001a\001b\nl3', eval([[system('cat -', ["l1\np2", "line2\na\nb", 'l3'])]])) @@ -202,7 +263,7 @@ describe('system()', function() describe("with a program that doesn't close stdout", function() if not xclip then - pending('skipped (missing xclip)', function() end) + pending('missing `xclip`', function() end) else it('will exit properly after passing input', function() eq('', eval([[system('xclip -i -selection clipboard', 'clip-data')]])) @@ -210,18 +271,12 @@ describe('system()', function() end) end end) - - describe('command passed as a list', function() - it('does not execute &shell', function() - eq('* $NOTHING ~/file', - eval("system(['echo', '-n', '*', '$NOTHING', '~/file'])")) - end) - end) end) +if helpers.pending_win32(pending) then return end + describe('systemlist()', function() - -- behavior is similar to `system()` but it returns a list instead of a - -- string. + -- Similar to `system()`, but returns List instead of String. before_each(clear) it('sets the v:shell_error variable', function() @@ -334,14 +389,14 @@ describe('systemlist()', function() end) end) - describe('passing list as input', function() + describe('input passed as List', function() it('joins list items with linefeed characters', function() eq({'line1', 'line2', 'line3'}, eval("systemlist('cat -', ['line1', 'line2', 'line3'])")) end) -- Unlike `system()` which uses SOH to represent NULs, with `systemlist()` - -- input and ouput are the same + -- input and ouput are the same. describe('with linefeed characters inside list items', function() it('converts linefeed characters to NULs', function() eq({'l1\np2', 'line2\na\nb', 'l3'}, @@ -381,7 +436,7 @@ describe('systemlist()', function() describe("with a program that doesn't close stdout", function() if not xclip then - pending('skipped (missing xclip)', function() end) + pending('missing `xclip`', function() end) else it('will exit properly after passing input', function() eq({}, eval( 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/dict_notifications_spec.lua b/test/functional/ex_cmds/dict_notifications_spec.lua index e6f7609016..5e89986c0f 100644 --- a/test/functional/ex_cmds/dict_notifications_spec.lua +++ b/test/functional/ex_cmds/dict_notifications_spec.lua @@ -3,6 +3,7 @@ local clear, nvim, source = helpers.clear, helpers.nvim, helpers.source local eq, next_msg = helpers.eq, helpers.next_message local exc_exec = helpers.exc_exec local command = helpers.command +local eval = helpers.eval describe('dictionary change notifications', function() @@ -255,5 +256,21 @@ describe('dictionary change notifications', function() eq({'notification', '2b', {'key', {old = 'v2', new = 'value'}}}, next_msg()) end) + + it('does not crash when freeing a watched dictionary', function() + source([[ + function! Watcher(dict, key, value) + echo a:key string(a:value) + endfunction + + function! MakeWatch() + let d = {'foo': 'bar'} + call dictwatcheradd(d, 'foo', function('Watcher')) + endfunction + ]]) + + command('call MakeWatch()') + eq(2, eval('1+1')) -- Still alive? + 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/fixtures/CMakeLists.txt b/test/functional/fixtures/CMakeLists.txt index 70aee6efa9..8537ea390f 100644 --- a/test/functional/fixtures/CMakeLists.txt +++ b/test/functional/fixtures/CMakeLists.txt @@ -2,3 +2,4 @@ add_executable(tty-test tty-test.c) target_link_libraries(tty-test ${LIBUV_LIBRARIES}) add_executable(shell-test shell-test.c) +add_executable(printargs-test printargs-test.c) diff --git a/test/functional/fixtures/printargs-test.c b/test/functional/fixtures/printargs-test.c new file mode 100644 index 0000000000..2c25cf8447 --- /dev/null +++ b/test/functional/fixtures/printargs-test.c @@ -0,0 +1,9 @@ +#include <stdio.h> + +int main(int argc, char **argv) +{ + for (int i=1; i<argc; i++) { + printf("arg%d=%s;", i, argv[i]); + } + return 0; +} diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index f3332cff4f..2939184d2c 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) @@ -380,6 +346,14 @@ local function source(code) return fname end +local function set_shell_powershell() + source([[ + set shell=powershell shellquote=\" shellpipe=\| shellredir=> + set shellcmdflag=\ -ExecutionPolicy\ RemoteSigned\ -Command + let &shellxquote=' ' + ]]) +end + local function nvim(method, ...) return request('nvim_'..method, ...) end @@ -475,6 +449,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 +500,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 +513,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 +597,8 @@ return function(after_each) curwinmeths = curwinmeths, curtabmeths = curtabmeths, pending_win32 = pending_win32, + skip_fragile = skip_fragile, + set_shell_powershell = set_shell_powershell, 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/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua index 35aeb6e67c..41ebfd2334 100644 --- a/test/functional/ui/inccommand_spec.lua +++ b/test/functional/ui/inccommand_spec.lua @@ -42,6 +42,7 @@ local function common_setup(screen, inccommand, text) [14] = {foreground = Screen.colors.White, background = Screen.colors.Red}, [15] = {bold=true, foreground=Screen.colors.Blue}, [16] = {background=Screen.colors.Grey90}, -- cursorline + vis = {background=Screen.colors.LightGrey} }) end @@ -207,6 +208,42 @@ describe(":substitute, 'inccommand' preserves", function() end) end + for _, case in pairs{"", "split", "nosplit"} do + it("visual selection for non-previewable command (inccommand="..case..") #5888", function() + local screen = Screen.new(30,10) + common_setup(screen, case, default_text) + feed('1G2V') + + feed(':s') + screen:expect([[ + {vis:Inc substitution on} | + t{vis:wo lines} | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :'<,'>s^ | + ]]) + + feed('o') + screen:expect([[ + {vis:Inc substitution on} | + t{vis:wo lines} | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :'<,'>so^ | + ]]) + end) + end + end) describe(":substitute, 'inccommand' preserves undo", function() @@ -1201,6 +1238,40 @@ describe(":substitute, 'inccommand' with a failing expression", function() end end) + it('in the range does not error #5912', function() + for _, case in pairs(cases) do + refresh(case) + feed(':100s/') + + screen:expect([[ + Inc substitution on | + two lines | + | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + :100s/^ | + ]]) + + feed('<enter>') + screen:expect([[ + Inc substitution on | + two lines | + ^ | + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {15:~ }| + {14:E16: Invalid range} | + ]]) + end + end) + end) describe("'inccommand' and :cnoremap", function() 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/functional/ui/screen.lua b/test/functional/ui/screen.lua index 2581b36711..ef1f0783e7 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -1,31 +1,17 @@ --- This module contains the Screen class, a complete Nvim screen implementation --- designed for functional testing. The goal is to provide a simple and --- intuitive API for verifying screen state after a set of actions. +-- This module contains the Screen class, a complete Nvim UI implementation +-- designed for functional testing (verifying screen state, in particular). -- --- The screen class exposes a single assertion method, "Screen:expect". This --- method takes a string representing the expected screen state and an optional --- set of attribute identifiers for checking highlighted characters(more on --- this later). --- --- The string passed to "expect" will be processed according to these rules: --- --- - Each line of the string represents and is matched individually against --- a screen row. --- - The entire string is stripped of common indentation --- - Expected screen rows are stripped of the last character. The last --- character should be used to write pipes(|) that make clear where the --- screen ends --- - The last line is stripped, so the string must have (row count + 1) --- lines. +-- Screen:expect() takes a string representing the expected screen state and an +-- optional set of attribute identifiers for checking highlighted characters. -- -- Example usage: -- -- local screen = Screen.new(25, 10) --- -- attach the screen to the current Nvim instance +-- -- Attach the screen to the current Nvim instance. -- screen:attach() --- --enter insert mode and type some text +-- -- Enter insert-mode and type some text. -- feed('ihello screen') --- -- declare an expectation for the eventual screen state +-- -- Assert the expected screen state. -- screen:expect([[ -- hello screen | -- ~ | @@ -39,31 +25,19 @@ -- -- INSERT -- | -- ]]) -- <- Last line is stripped -- --- Since screen updates are received asynchronously, "expect" is actually --- specifying the eventual screen state. This is how "expect" works: It will --- start the event loop with a timeout of 5 seconds. Each time it receives an --- update the expected state will be checked against the updated state. --- --- If the expected state matches the current state, the event loop will be --- stopped and "expect" will return. If the timeout expires, the last match --- error will be reported and the test will fail. +-- Since screen updates are received asynchronously, expect() actually specifies +-- the _eventual_ screen state. -- --- If the second argument is passed to "expect", the screen rows will be --- transformed before being matched against the string lines. The --- transformation rule is simple: Each substring "S" composed with characters --- having the exact same set of attributes will be substituted by "{K:S}", --- where K is a key associated the attribute set via the second argument of --- "expect". --- If a transformation table is present, unexpected attribute sets in the final --- state is considered an error. To make testing simpler, a list of attribute --- sets that should be ignored can be passed as a third argument. Alternatively, --- this third argument can be "true" to indicate that all unexpected attribute --- sets should be ignored. +-- This is how expect() works: +-- * It starts the event loop with a timeout. +-- * Each time it receives an update it checks that against the expected state. +-- * If the expected state matches the current state, the event loop will be +-- stopped and expect() will return. +-- * If the timeout expires, the last match error will be reported and the +-- test will fail. -- --- To illustrate how this works, let's say that in the above example we wanted --- to assert that the "-- INSERT --" string is highlighted with the bold --- attribute(which normally is), here's how the call to "expect" should look --- like: +-- Continuing the above example, say we want to assert that "-- INSERT --" is +-- highlighted with the bold attribute. The expect() call should look like this: -- -- NonText = Screen.colors.Blue -- screen:expect([[ @@ -81,29 +55,20 @@ -- -- In this case "b" is a string associated with the set composed of one -- attribute: bold. Note that since the {b:} markup is not a real part of the --- screen, the delimiter(|) had to be moved right. Also, the highlighting of the --- NonText markers (~) is ignored in this test. +-- screen, the delimiter "|" moved to the right. Also, the highlighting of the +-- NonText markers "~" is ignored in this test. +-- +-- Tests will often share a group of attribute sets to expect(). Those can be +-- defined at the beginning of a test: -- --- Multiple expect:s will likely share a group of attribute sets to test. --- Therefore these could be specified at the beginning of a test like this: -- NonText = Screen.colors.Blue -- screen:set_default_attr_ids( { -- [1] = {reverse = true, bold = true}, -- [2] = {reverse = true} -- }) -- screen:set_default_attr_ignore( {{}, {bold=true, foreground=NonText}} ) --- These can be overridden for a specific expect expression, by passing --- different sets as parameters. -- --- To help writing screen tests, there is a utility function --- "screen:snapshot_util()", that can be placed in a test file at any point an --- "expect(...)" should be. It will wait a short amount of time and then dump --- the current state of the screen, in the form of an "expect(..)" expression --- that would match it exactly. "snapshot_util" optionally also take the --- transformation and ignore set as parameters, like expect, or uses the default --- set. It will generate a larger attribute transformation set, if needed. --- To generate a text-only test without highlight checks, --- use `screen:snapshot_util({},true)` +-- To help write screen tests, see screen:snapshot_util(). local helpers = require('test.functional.helpers')(nil) local request, run, uimeths = helpers.request, helpers.run, helpers.uimeths @@ -205,13 +170,22 @@ end function Screen:try_resize(columns, rows) uimeths.try_resize(columns, rows) + -- Give ourselves a chance to _handle_resize, which requires using + -- self.sleep() (for the resize notification) rather than run() + self:sleep(0.1) end -- Asserts that `expected` eventually matches the screen state. -- --- expected: Expected screen state (string). --- attr_ids: Text attribute definitions. --- attr_ignore: Ignored text attributes. +-- expected: Expected screen state (string). Each line represents a screen +-- row. Last character of each row (typically "|") is stripped. +-- Common indentation is stripped. +-- 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 +-- "{K:S}", where K is a key in `attr_ids`. Any unexpected +-- attributes in the final state are an error. +-- attr_ignore: Ignored text attributes, or `true` to ignore all. -- condition: Function asserting some arbitrary condition. -- any: true: Succeed if `expected` matches ANY screen line(s). -- false (default): `expected` must match screen exactly. @@ -224,6 +198,11 @@ function Screen:expect(expected, attr_ids, attr_ignore, condition, any) 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.') + end local ids = attr_ids or self._default_attr_ids local ignore = attr_ignore or self._default_attr_ignore self:wait(function() @@ -541,8 +520,10 @@ function Screen:_current_screen() return table.concat(rv, '\n') end +-- Utility to generate/debug tests. Call it where screen:expect() would be. +-- Waits briefly, then dumps the current screen state in the form of +-- screen:expect(). Use snapshot_util({},true) to generate a text-only test. function Screen:snapshot_util(attrs, ignore) - -- util to generate screen test self:sleep(250) self:print_snapshot(attrs, ignore) end diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua index d03f98c26f..0824585717 100644 --- a/test/functional/ui/screen_basic_spec.lua +++ b/test/functional/ui/screen_basic_spec.lua @@ -4,8 +4,7 @@ local spawn, set_session, clear = helpers.spawn, helpers.set_session, helpers.cl local feed, execute = helpers.feed, helpers.execute local insert = helpers.insert local eq = helpers.eq - -if helpers.pending_win32(pending) then return end +local eval = helpers.eval describe('Initial screen', function() local screen @@ -563,11 +562,10 @@ describe('Screen', function() ]]) end) - -- FIXME this has some race conditions that cause it to fail periodically - pending('has minimum width/height values', function() + it('has minimum width/height values', function() screen:try_resize(1, 1) screen:expect([[ - -- INS^ERT --| + {2:-- INS^ERT --}| | ]]) feed('<esc>:ls') @@ -690,4 +688,12 @@ describe('Screen', function() end) end) end) + + it('nvim_ui_attach() handles very large width/height #2180', function() + screen:detach() + screen = Screen.new(999, 999) + screen:attach() + eq(999, eval('&lines')) + eq(999, eval('&columns')) + end) 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(...) |