diff options
author | James McCoy <jamessan@jamessan.com> | 2018-03-28 21:52:06 -0400 |
---|---|---|
committer | James McCoy <jamessan@jamessan.com> | 2018-03-28 21:54:39 -0400 |
commit | 79f9c2d9c650ceab27cdc6707fd6d7fa1de29fc1 (patch) | |
tree | 4e0589d75801f3ff6a9678f84f5009102766661e /test/functional/core | |
parent | 4403864da3c48412595d439f36458d1e6ccfc49f (diff) | |
parent | 3f3de9b1a95d273463a87516365510dbffcaf3d2 (diff) | |
download | rneovim-79f9c2d9c650ceab27cdc6707fd6d7fa1de29fc1.tar.gz rneovim-79f9c2d9c650ceab27cdc6707fd6d7fa1de29fc1.tar.bz2 rneovim-79f9c2d9c650ceab27cdc6707fd6d7fa1de29fc1.zip |
Merge branch 'master' into yagebu/option-fixes
Diffstat (limited to 'test/functional/core')
-rw-r--r-- | test/functional/core/channels_spec.lua | 281 | ||||
-rw-r--r-- | test/functional/core/exit_spec.lua | 62 | ||||
-rw-r--r-- | test/functional/core/job_spec.lua | 198 | ||||
-rw-r--r-- | test/functional/core/path_spec.lua | 56 | ||||
-rw-r--r-- | test/functional/core/startup_spec.lua | 96 |
5 files changed, 644 insertions, 49 deletions
diff --git a/test/functional/core/channels_spec.lua b/test/functional/core/channels_spec.lua new file mode 100644 index 0000000000..f79f208b69 --- /dev/null +++ b/test/functional/core/channels_spec.lua @@ -0,0 +1,281 @@ + +local helpers = require('test.functional.helpers')(after_each) +local clear, eq, eval, next_msg, ok, source = helpers.clear, helpers.eq, + helpers.eval, helpers.next_msg, helpers.ok, helpers.source +local command, funcs, meths = helpers.command, helpers.funcs, helpers.meths +local sleep = helpers.sleep +local spawn, nvim_argv = helpers.spawn, helpers.nvim_argv +local set_session = helpers.set_session +local nvim_prog = helpers.nvim_prog +local retry = helpers.retry +local expect_twostreams = helpers.expect_twostreams + +describe('channels', function() + local init = [[ + function! Normalize(data) abort + " Windows: remove ^M + return type([]) == type(a:data) + \ ? map(a:data, 'substitute(v:val, "\r", "", "g")') + \ : a:data + endfunction + function! OnEvent(id, data, event) dict + call rpcnotify(1, a:event, a:id, a:data) + endfunction + ]] + before_each(function() + clear() + source(init) + end) + + pending('can connect to socket', function() + local server = spawn(nvim_argv) + set_session(server) + local address = funcs.serverlist()[1] + local client = spawn(nvim_argv) + set_session(client, true) + source(init) + + meths.set_var('address', address) + command("let g:id = sockconnect('pipe', address, {'on_data':'OnEvent'})") + local id = eval("g:id") + ok(id > 0) + + command("call chansend(g:id, msgpackdump([[2,'nvim_set_var',['code',23]]]))") + set_session(server, true) + retry(nil, 1000, function() + eq(23, meths.get_var('code')) + end) + set_session(client, true) + + command("call chansend(g:id, msgpackdump([[0,0,'nvim_eval',['2+3']]]))") + + + local res = eval("msgpackdump([[1,0,v:null,5]])") + eq({"\148\001\n\192\005"}, res) + eq({'notification', 'data', {id, res}}, next_msg()) + command("call chansend(g:id, msgpackdump([[2,'nvim_command',['quit']]]))") + eq({'notification', 'data', {id, {''}}}, next_msg()) + end) + + it('can use stdio channel', function() + source([[ + let g:job_opts = { + \ 'on_stdout': function('OnEvent'), + \ 'on_stderr': function('OnEvent'), + \ 'on_exit': function('OnEvent'), + \ } + ]]) + meths.set_var("nvim_prog", nvim_prog) + meths.set_var("code", [[ + function! OnEvent(id, data, event) dict + let text = string([a:id, a:data, a:event]) + call chansend(g:x, text) + + if a:data == [''] + call chansend(v:stderr, "*dies*") + quit + endif + endfunction + let g:x = stdioopen({'on_stdin':'OnEvent'}) + call chansend(x, "hello") + ]]) + command("let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)") + local id = eval("g:id") + ok(id > 0) + + eq({ "notification", "stdout", {id, { "hello" } } }, next_msg()) + + command("call chansend(id, 'howdy')") + eq({"notification", "stdout", {id, {"[1, ['howdy'], 'stdin']"}}}, next_msg()) + + command("call chanclose(id, 'stdin')") + expect_twostreams({{"notification", "stdout", {id, {"[1, [''], 'stdin']"}}}, + {'notification', 'stdout', {id, {''}}}}, + {{"notification", "stderr", {id, {"*dies*"}}}, + {'notification', 'stderr', {id, {''}}}}) + eq({"notification", "exit", {3,0}}, next_msg()) + end) + + local function expect_twoline(id, stream, line1, line2, nobr) + local msg = next_msg() + local joined = nobr and {line1..line2} or {line1, line2} + if not pcall(eq, {"notification", stream, {id, joined}}, msg) then + local sep = (not nobr) and "" or nil + eq({"notification", stream, {id, {line1, sep}}}, msg) + eq({"notification", stream, {id, {line2}}}, next_msg()) + end + end + + it('can use stdio channel with pty', function() + if helpers.pending_win32(pending) then return end + source([[ + let g:job_opts = { + \ 'on_stdout': function('OnEvent'), + \ 'on_exit': function('OnEvent'), + \ 'pty': v:true, + \ } + ]]) + meths.set_var("nvim_prog", nvim_prog) + meths.set_var("code", [[ + function! OnEvent(id, data, event) dict + let text = string([a:id, a:data, a:event]) + call chansend(g:x, text) + endfunction + let g:x = stdioopen({'on_stdin':'OnEvent'}) + ]]) + command("let g:id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)") + local id = eval("g:id") + ok(id > 0) + + command("call chansend(id, 'TEXT\n')") + expect_twoline(id, "stdout", "TEXT\r", "[1, ['TEXT', ''], 'stdin']") + + + command("call chansend(id, 'neovan')") + eq({"notification", "stdout", {id, {"neovan"}}}, next_msg()) + command("call chansend(id, '\127\127im\n')") + expect_twoline(id, "stdout", "\b \b\b \bim\r", "[1, ['neovim', ''], 'stdin']") + + command("call chansend(id, 'incomplet\004')") + + local is_freebsd = eval("system('uname') =~? 'FreeBSD'") == 1 + local bsdlike = is_freebsd or (helpers.os_name() == "osx") + print("bsdlike:", bsdlike) + local extra = bsdlike and "^D\008\008" or "" + expect_twoline(id, "stdout", + "incomplet"..extra, "[1, ['incomplet'], 'stdin']", true) + + + command("call chansend(id, '\004')") + if bsdlike then + expect_twoline(id, "stdout", extra, "[1, [''], 'stdin']", true) + else + eq({"notification", "stdout", {id, {"[1, [''], 'stdin']"}}}, next_msg()) + end + + -- channel is still open + command("call chansend(id, 'hi again!\n')") + eq({"notification", "stdout", {id, {"hi again!\r", ""}}}, next_msg()) + end) + + + it('stdio channel can use rpc and stderr simultaneously', function() + if helpers.pending_win32(pending) then return end + source([[ + let g:job_opts = { + \ 'on_stderr': function('OnEvent'), + \ 'on_exit': function('OnEvent'), + \ 'rpc': v:true, + \ } + ]]) + meths.set_var("nvim_prog", nvim_prog) + meths.set_var("code", [[ + let id = stdioopen({'rpc':v:true}) + call rpcnotify(id,"nvim_call_function", "rpcnotify", [1, "message", "hi there!", id]) + call chansend(v:stderr, "trouble!") + ]]) + command("let id = jobstart([ g:nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', 'set noswapfile', '--headless', '--cmd', g:code], g:job_opts)") + eq({"notification", "message", {"hi there!", 1}}, next_msg()) + eq({"notification", "stderr", {3, {"trouble!"}}}, next_msg()) + + eq(30, eval("rpcrequest(id, 'nvim_eval', '[chansend(v:stderr, \"math??\"), 5*6][1]')")) + eq({"notification", "stderr", {3, {"math??"}}}, next_msg()) + + local _, err = pcall(command,"call rpcrequest(id, 'nvim_command', 'call chanclose(v:stderr, \"stdin\")')") + ok(string.find(err,"E906: invalid stream for channel") ~= nil) + + eq(1, eval("rpcrequest(id, 'nvim_eval', 'chanclose(v:stderr, \"stderr\")')")) + eq({"notification", "stderr", {3, {""}}}, next_msg()) + + command("call rpcnotify(id, 'nvim_command', 'quit')") + eq({"notification", "exit", {3, 0}}, next_msg()) + end) + + it('can use buffered output mode', function() + if helpers.pending_win32(pending) then return end + source([[ + let g:job_opts = { + \ 'on_stdout': function('OnEvent'), + \ 'on_exit': function('OnEvent'), + \ 'stdout_buffered': v:true, + \ } + ]]) + command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)") + local id = eval("g:id") + + command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]]) + sleep(10) + command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]]) + command("call chanclose(id, 'stdin')") + + eq({"notification", "stdout", {id, {'10 PRINT "NVIM"', + '20 GOTO 10', ''}}}, next_msg()) + eq({"notification", "exit", {id, 0}}, next_msg()) + + command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)") + id = eval("g:id") + + command([[call chansend(id, "is no number\nnot at all")]]) + command("call chanclose(id, 'stdin')") + + -- works correctly with no output + eq({"notification", "stdout", {id, {''}}}, next_msg()) + eq({"notification", "exit", {id, 1}}, next_msg()) + + end) + + it('can use buffered output mode with no stream callback', function() + if helpers.pending_win32(pending) then return end + source([[ + function! OnEvent(id, data, event) dict + call rpcnotify(1, a:event, a:id, a:data, self.stdout) + endfunction + let g:job_opts = { + \ 'on_exit': function('OnEvent'), + \ 'stdout_buffered': v:true, + \ } + ]]) + command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)") + local id = eval("g:id") + + command([[call chansend(id, "stuff\n10 PRINT \"NVIM\"\nxx")]]) + sleep(10) + command([[call chansend(id, "xx\n20 GOTO 10\nzz\n")]]) + command("call chanclose(id, 'stdin')") + + eq({"notification", "exit", {id, 0, {'10 PRINT "NVIM"', + '20 GOTO 10', ''}}}, next_msg()) + + -- if dict is reused the new value is not stored, + -- but nvim also does not crash + command("let id = jobstart(['cat'], g:job_opts)") + id = eval("g:id") + + command([[call chansend(id, "cat text\n")]]) + sleep(10) + command("call chanclose(id, 'stdin')") + + -- old value was not overwritten + eq({"notification", "exit", {id, 0, {'10 PRINT "NVIM"', + '20 GOTO 10', ''}}}, next_msg()) + + -- and an error was thrown. + eq("E5210: dict key 'stdout' already set for buffered stream in channel "..id, eval('v:errmsg')) + + -- reset dictionary + source([[ + let g:job_opts = { + \ 'on_exit': function('OnEvent'), + \ 'stdout_buffered': v:true, + \ } + ]]) + command("let id = jobstart(['grep', '^[0-9]'], g:job_opts)") + id = eval("g:id") + + command([[call chansend(id, "is no number\nnot at all")]]) + command("call chanclose(id, 'stdin')") + + -- works correctly with no output + eq({"notification", "exit", {id, 1, {''}}}, next_msg()) + end) +end) diff --git a/test/functional/core/exit_spec.lua b/test/functional/core/exit_spec.lua index 3fb39f3e78..80c65e4544 100644 --- a/test/functional/core/exit_spec.lua +++ b/test/functional/core/exit_spec.lua @@ -1,9 +1,14 @@ local helpers = require('test.functional.helpers')(after_each) local command = helpers.command +local feed_command = helpers.feed_command local eval = helpers.eval -local eq, neq = helpers.eq, helpers.neq +local eq = helpers.eq local run = helpers.run +local funcs = helpers.funcs +local nvim_prog = helpers.nvim_prog +local redir_exec = helpers.redir_exec +local wait = helpers.wait describe('v:exiting', function() local cid @@ -29,18 +34,65 @@ describe('v:exiting', function() end run(on_request, nil, on_setup) end) - - it('is non-zero after :cquit', function() + it('is 0 on exit from ex-mode involving try-catch', function() local function on_setup() command('autocmd VimLeavePre * call rpcrequest('..cid..', "")') command('autocmd VimLeave * call rpcrequest('..cid..', "")') - command('cquit') + feed_command('call feedkey("Q")','try', 'call NoFunction()', 'catch', 'echo "bye"', 'endtry', 'quit') end local function on_request() - neq(0, eval('v:exiting')) + eq(0, eval('v:exiting')) return '' end run(on_request, nil, on_setup) end) +end) + +describe(':cquit', function() + local function test_cq(cmdline, exit_code, redir_msg) + if redir_msg then + eq('\n' .. redir_msg, redir_exec(cmdline)) + wait() + eq(2, eval("1+1")) -- Still alive? + else + funcs.system({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--headless', '--cmd', cmdline}) + eq(exit_code, eval('v:shell_error')) + end + end + + before_each(function() + helpers.clear() + end) + + it('exits with non-zero after :cquit', function() + test_cq('cquit', 1, nil) + end) + + it('exits with non-zero after :cquit 123', function() + test_cq('cquit 123', 123, nil) + end) + + it('exits with non-zero after :123 cquit', function() + test_cq('123 cquit', 123, nil) + end) + + it('exits with 0 after :cquit 0', function() + test_cq('cquit 0', 0, nil) + end) + + it('exits with 0 after :0 cquit', function() + test_cq('0 cquit', 0, nil) + end) + + it('exits with redir msg for multiple exit codes after :cquit 1 2', function() + test_cq('cquit 1 2', nil, 'E488: Trailing characters: cquit 1 2') + end) + it('exits with redir msg for non-number exit code after :cquit X', function() + test_cq('cquit X', nil, 'E488: Trailing characters: cquit X') + end) + + it('exits with redir msg for negative exit code after :cquit -1', function() + test_cq('cquit -1', nil, 'E488: Trailing characters: cquit -1') + end) end) diff --git a/test/functional/core/job_spec.lua b/test/functional/core/job_spec.lua index b98169b067..6d4cadbdc8 100644 --- a/test/functional/core/job_spec.lua +++ b/test/functional/core/job_spec.lua @@ -2,13 +2,21 @@ local helpers = require('test.functional.helpers')(after_each) local clear, eq, eval, exc_exec, feed_command, feed, insert, neq, next_msg, nvim, nvim_dir, ok, source, write_file, mkdir, rmdir = helpers.clear, helpers.eq, helpers.eval, helpers.exc_exec, helpers.feed_command, helpers.feed, - helpers.insert, helpers.neq, helpers.next_message, helpers.nvim, + helpers.insert, helpers.neq, helpers.next_msg, helpers.nvim, helpers.nvim_dir, helpers.ok, helpers.source, helpers.write_file, helpers.mkdir, helpers.rmdir local command = helpers.command +local funcs = helpers.funcs +local retry = helpers.retry +local meths = helpers.meths +local NIL = helpers.NIL local wait = helpers.wait local iswin = helpers.iswin local get_pathsep = helpers.get_pathsep +local pathroot = helpers.pathroot +local nvim_set = helpers.nvim_set +local expect_twostreams = helpers.expect_twostreams +local expect_msg_seq = helpers.expect_msg_seq local Screen = require('test.functional.ui.screen') describe('jobs', function() @@ -28,15 +36,14 @@ describe('jobs', function() \ ? map(a:data, 'substitute(v:val, "\r", "", "g")') \ : a:data endfunction - function! s:OnEvent(id, data, event) dict + function! OnEvent(id, data, event) dict let userdata = get(self, 'user') let data = Normalize(a:data) call rpcnotify(g:channel, a:event, userdata, data) endfunction let g:job_opts = { - \ 'on_stdout': function('s:OnEvent'), - \ 'on_stderr': function('s:OnEvent'), - \ 'on_exit': function('s:OnEvent'), + \ 'on_stdout': function('OnEvent'), + \ 'on_exit': function('OnEvent'), \ 'user': 0 \ } ]]) @@ -50,18 +57,20 @@ describe('jobs', function() nvim('command', "let j = jobstart('echo $VAR', g:job_opts)") end eq({'notification', 'stdout', {0, {'abc', ''}}}, next_msg()) + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) it('changes to given / directory', function() nvim('command', "let g:job_opts.cwd = '/'") if iswin() then - nvim('command', "let j = jobstart('pwd|%{$_.Path}', g:job_opts)") + nvim('command', "let j = jobstart('(Get-Location).Path', g:job_opts)") else nvim('command', "let j = jobstart('pwd', g:job_opts)") end eq({'notification', 'stdout', - {0, {(iswin() and [[C:\]] or '/'), ''}}}, next_msg()) + {0, {pathroot(), ''}}}, next_msg()) + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -70,12 +79,22 @@ describe('jobs', function() mkdir(dir) nvim('command', "let g:job_opts.cwd = '" .. dir .. "'") if iswin() then - nvim('command', "let j = jobstart('pwd|%{$_.Path}', g:job_opts)") + nvim('command', "let j = jobstart('(Get-Location).Path', g:job_opts)") else nvim('command', "let j = jobstart('pwd', g:job_opts)") end - eq({'notification', 'stdout', {0, {dir, ''}}}, next_msg()) - eq({'notification', 'exit', {0, 0}}, next_msg()) + expect_msg_seq( + { {'notification', 'stdout', {0, {dir, ''} } }, + {'notification', 'stdout', {0, {''} } }, + {'notification', 'exit', {0, 0} } + }, + -- Alternative sequence: + { {'notification', 'stdout', {0, {dir} } }, + {'notification', 'stdout', {0, {'', ''} } }, + {'notification', 'stdout', {0, {''} } }, + {'notification', 'exit', {0, 0} } + } + ) rmdir(dir) end) @@ -100,13 +119,13 @@ describe('jobs', function() end) it('returns -1 when target is not executable #5465', function() - if helpers.pending_win32(pending) then return end local function new_job() return eval([[jobstart('')]]) end local executable_jobid = new_job() - local nonexecutable_jobid = eval( - "jobstart(['./test/functional/fixtures/non_executable.txt'])") + local nonexecutable_jobid = eval("jobstart(['"..(iswin() + and './test/functional/fixtures' + or './test/functional/fixtures/non_executable.txt').."'])") eq(-1, nonexecutable_jobid) -- Should _not_ throw an error. eq("", eval("v:errmsg")) @@ -117,8 +136,11 @@ describe('jobs', function() it('invokes callbacks when the job writes and exits', function() -- TODO: hangs on Windows if helpers.pending_win32(pending) then return end - nvim('command', "call jobstart('echo', g:job_opts)") - eq({'notification', 'stdout', {0, {'', ''}}}, next_msg()) + nvim('command', "let g:job_opts.on_stderr = function('OnEvent')") + nvim('command', [[call jobstart('echo ""', g:job_opts)]]) + expect_twostreams({{'notification', 'stdout', {0, {'', ''}}}, + {'notification', 'stdout', {0, {''}}}}, + {{'notification', 'stderr', {0, {''}}}}) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -133,6 +155,7 @@ describe('jobs', function() nvim('command', 'call jobsend(j, [123, "xyz", ""])') eq({'notification', 'stdout', {0, {'123', 'xyz', ''}}}, next_msg()) nvim('command', "call jobstop(j)") + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -144,6 +167,7 @@ describe('jobs', function() nvim('command', "let j = jobstart(['cat', '"..filename.."'], g:job_opts)") eq({'notification', 'stdout', {0, {'abc\ndef', ''}}}, next_msg()) + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) os.remove(filename) @@ -167,6 +191,7 @@ describe('jobs', function() nvim('command', 'call jobsend(j, "abc\\nxyz")') eq({'notification', 'stdout', {0, {'abc', 'xyz'}}}, next_msg()) nvim('command', "call jobstop(j)") + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -185,6 +210,7 @@ describe('jobs', function() eq({'notification', 'stdout', {0, {'\n123\n', 'abc\nxyz\n', ''}}}, next_msg()) nvim('command', "call jobstop(j)") + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -195,6 +221,7 @@ describe('jobs', function() eq({'notification', 'stdout', {0, {'some data', 'without\nfinal nl'}}}, next_msg()) nvim('command', "call jobstop(j)") + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -202,6 +229,7 @@ describe('jobs', function() if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") nvim('command', 'call jobclose(j, "stdin")') + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) end) @@ -228,7 +256,6 @@ describe('jobs', function() end) it('will not leak memory if we leave a job running', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. nvim('command', "call jobstart(['cat', '-'], g:job_opts)") end) @@ -238,6 +265,7 @@ describe('jobs', function() local pid = eval('jobpid(j)') eq(0,os.execute('ps -p '..pid..' > /dev/null')) nvim('command', 'call jobstop(j)') + eq({'notification', 'stdout', {0, {''}}}, next_msg()) eq({'notification', 'exit', {0, 0}}, next_msg()) neq(0,os.execute('ps -p '..pid..' > /dev/null')) end) @@ -268,21 +296,21 @@ describe('jobs', function() nvim('command', 'let g:job_opts.user = {"n": 5, "s": "str", "l": [1]}') nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) local data = {n = 5, s = 'str', l = {1}} - eq({'notification', 'stdout', {data, {'foo', ''}}}, next_msg()) + expect_msg_seq( + { {'notification', 'stdout', {data, {'foo', ''}}}, + {'notification', 'stdout', {data, {''}}}, + }, + -- Alternative sequence: + { {'notification', 'stdout', {data, {'foo'}}}, + {'notification', 'stdout', {data, {'', ''}}}, + {'notification', 'stdout', {data, {''}}}, + } + ) eq({'notification', 'exit', {data, 0}}, next_msg()) end) - it('can omit options', function() - if helpers.pending_win32(pending) then return end - neq(0, nvim('eval', 'delete(".Xtestjob")')) - nvim('command', "call jobstart(['touch', '.Xtestjob'])") - nvim('command', "sleep 100m") - eq(0, nvim('eval', 'delete(".Xtestjob")')) - end) - it('can omit data callbacks', function() nvim('command', 'unlet g:job_opts.on_stdout') - nvim('command', 'unlet g:job_opts.on_stderr') nvim('command', 'let g:job_opts.user = 5') nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) eq({'notification', 'exit', {5, 0}}, next_msg()) @@ -292,12 +320,22 @@ describe('jobs', function() nvim('command', 'unlet g:job_opts.on_exit') nvim('command', 'let g:job_opts.user = 5') nvim('command', [[call jobstart('echo "foo"', g:job_opts)]]) - eq({'notification', 'stdout', {5, {'foo', ''}}}, next_msg()) + expect_msg_seq( + { {'notification', 'stdout', {5, {'foo', ''} } }, + {'notification', 'stdout', {5, {''} } }, + }, + -- Alternative sequence: + { {'notification', 'stdout', {5, {'foo'} } }, + {'notification', 'stdout', {5, {'', ''} } }, + {'notification', 'stdout', {5, {''} } }, + } + ) end) it('will pass return code with the exit event', function() nvim('command', 'let g:job_opts.user = 5') nvim('command', "call jobstart('exit 55', g:job_opts)") + eq({'notification', 'stdout', {5, {''}}}, next_msg()) eq({'notification', 'exit', {5, 55}}, next_msg()) end) @@ -313,7 +351,6 @@ describe('jobs', function() end) it('can redefine callbacks being used by a job', function() - if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. local screen = Screen.new() screen:attach() screen:set_default_attr_ids({ @@ -328,7 +365,7 @@ describe('jobs', function() \ 'on_stderr': function('g:JobHandler'), \ 'on_exit': function('g:JobHandler') \ } - let job = jobstart('cat -', g:callbacks) + let job = jobstart(['cat', '-'], g:callbacks) ]]) wait() source([[ @@ -340,6 +377,14 @@ describe('jobs', function() end) it('requires funcrefs for script-local (s:) functions', function() + local screen = Screen.new(60, 5) + screen:attach() + screen:set_default_attr_ids({ + [1] = {bold = true, foreground = Screen.colors.Blue1}, + [2] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + [3] = {bold = true, foreground = Screen.colors.SeaGreen4} + }) + -- Pass job callback names _without_ `function(...)`. source([[ function! s:OnEvent(id, data, event) dict @@ -349,14 +394,10 @@ describe('jobs', function() \ 'on_stdout': 's:OnEvent', \ 'on_stderr': 's:OnEvent', \ 'on_exit': 's:OnEvent', - \ 'user': 2349 \ }) ]]) - -- The behavior is asynchronous, retry until a time limit. - helpers.retry(nil, 10000, function() - eq("E120:", string.match(eval("v:errmsg"), "E%d*:")) - end) + screen:expect("{2:E120: Using <SID> not in a script context: s:OnEvent}",nil,nil,nil,true) end) it('does not repeat output with slow output handlers', function() @@ -375,7 +416,7 @@ describe('jobs', function() call jobwait([jobstart(cmd, d)]) call rpcnotify(g:channel, 'data', d.data) ]]) - eq({'notification', 'data', {{{'1', ''}, {'2', ''}, {'3', ''}, {'4', ''}, {'5', ''}}}}, next_msg()) + eq({'notification', 'data', {{{'1', ''}, {'2', ''}, {'3', ''}, {'4', ''}, {'5', ''}, {''}}}}, next_msg()) end) it('jobstart() works with partial functions', function() @@ -389,7 +430,14 @@ describe('jobs', function() let g:job_opts = {'on_stdout': Callback} call jobstart('echo "some text"', g:job_opts) ]]) - eq({'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, next_msg()) + expect_msg_seq( + { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, + }, + -- Alternative sequence: + { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}}, + {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}}, + } + ) end) it('jobstart() works with closures', function() @@ -402,7 +450,14 @@ describe('jobs', function() let g:job_opts = {'on_stdout': MkFun()} call jobstart('echo "some text"', g:job_opts) ]]) - eq({'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, next_msg()) + expect_msg_seq( + { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, + }, + -- Alternative sequence: + { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}}, + {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}}, + } + ) end) it('jobstart() works when closure passed directly to `jobstart`', function() @@ -410,7 +465,14 @@ describe('jobs', function() let g:job_opts = {'on_stdout': {id, data, event -> rpcnotify(g:channel, '1', 'foo', 'bar', Normalize(data), event)}} call jobstart('echo "some text"', g:job_opts) ]]) - eq({'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, next_msg()) + expect_msg_seq( + { {'notification', '1', {'foo', 'bar', {'some text', ''}, 'stdout'}}, + }, + -- Alternative sequence: + { {'notification', '1', {'foo', 'bar', {'some text'}, 'stdout'}}, + {'notification', '1', {'foo', 'bar', {'', ''}, 'stdout'}}, + } + ) end) describe('jobwait', function() @@ -469,7 +531,7 @@ describe('jobs', function() eq({'notification', 'wait', {{-3, 5}}}, next_msg()) end) - it('will return -2 when interrupted', function() + it('will return -2 when interrupted without timeout', function() feed_command('call rpcnotify(g:channel, "ready") | '.. 'call rpcnotify(g:channel, "wait", '.. 'jobwait([jobstart("sleep 10; exit 55")]))') @@ -478,6 +540,15 @@ describe('jobs', function() eq({'notification', 'wait', {{-2}}}, next_msg()) end) + it('will return -2 when interrupted with timeout', function() + feed_command('call rpcnotify(g:channel, "ready") | '.. + 'call rpcnotify(g:channel, "wait", '.. + 'jobwait([jobstart("sleep 10; exit 55")], 10000))') + eq({'notification', 'ready', {}}, next_msg()) + feed('<c-c>') + eq({'notification', 'wait', {{-2}}}, next_msg()) + end) + it('can be called recursively', function() if helpers.pending_win32(pending) then return end -- TODO: Need `cat`. source([[ @@ -496,7 +567,8 @@ describe('jobs', function() elseif self.state == 2 let self.state = 3 call jobsend(a:id, "line3\n") - else + elseif self.state == 3 + let self.state = 4 call rpcnotify(g:channel, 'w', printf('job %d closed', self.counter)) call jobclose(a:id, 'stdin') endif @@ -551,6 +623,7 @@ describe('jobs', function() -- FIXME need to wait until jobsend succeeds before calling jobstop pending('will only emit the "exit" event after "stdout" and "stderr"', function() + nvim('command', "let g:job_opts.on_stderr = function('s:OnEvent')") nvim('command', "let j = jobstart(['cat', '-'], g:job_opts)") local jobid = nvim('eval', 'j') nvim('eval', 'jobsend(j, "abcdef")') @@ -567,6 +640,42 @@ describe('jobs', function() ok(string.find(err, "E475: Invalid argument: job cannot have both 'pty' and 'rpc' options set") ~= nil) end) + it('jobstop() kills entire process tree #6530', function() + command('set shell& shellcmdflag& shellquote& shellpipe& shellredir& shellxquote&') + + -- XXX: Using `nvim` isn't a good test, it reaps its children on exit. + -- local c = 'call jobstart([v:progpath, "-u", "NONE", "-i", "NONE", "--headless"])' + -- local j = eval("jobstart([v:progpath, '-u', 'NONE', '-i', 'NONE', '--headless', '-c', '" + -- ..c.."', '-c', '"..c.."'])") + + -- Create child with several descendants. + local j = (iswin() + and eval([=[jobstart('start /b cmd /c "ping 127.0.0.1 -n 1 -w 30000 > NUL"]=] + ..[=[ & start /b cmd /c "ping 127.0.0.1 -n 1 -w 40000 > NUL"]=] + ..[=[ & start /b cmd /c "ping 127.0.0.1 -n 1 -w 50000 > NUL"')]=]) + or eval("jobstart('sleep 30 | sleep 30 | sleep 30')")) + local ppid = funcs.jobpid(j) + local children + retry(nil, nil, function() + children = meths.get_proc_children(ppid) + eq(3, #children) + end) + -- Assert that nvim_get_proc() sees the children. + for _, child_pid in ipairs(children) do + local info = meths.get_proc(child_pid) + -- eq((iswin() and 'nvim.exe' or 'nvim'), info.name) + eq(ppid, info.ppid) + end + -- Kill the root of the tree. + funcs.jobstop(j) + -- Assert that the children were killed. + retry(nil, nil, function() + for _, child_pid in ipairs(children) do + eq(NIL, meths.get_proc(child_pid)) + end + end) + end) + describe('running tty-test program', function() if helpers.pending_win32(pending) then return end local function next_chunk() @@ -637,7 +746,7 @@ describe('jobs', function() -- there won't be any more messages, and the test would hang. helpers.sleep(100) local err = exc_exec('call jobpid(j)') - eq('Vim(call):E900: Invalid job id', err) + eq('Vim(call):E900: Invalid channel id', err) -- cleanup eq(other_pid, eval('jobpid(' .. other_jobid .. ')')) @@ -669,18 +778,19 @@ describe("pty process teardown", function() if helpers.pending_win32(pending) then return end -- Use a nested nvim (in :term) to test without --headless. feed_command(":terminal '"..helpers.nvim_prog + .."' -u NONE -i NONE --cmd '"..nvim_set.."' " -- Use :term again in the _nested_ nvim to get a PTY process. -- Use `sleep` to simulate a long-running child of the PTY. - .."' +terminal +'!(sleep 300 &)' +qa") + .."+terminal +'!(sleep 300 &)' +qa") -- Exiting should terminate all descendants (PTY, its children, ...). screen:expect([[ - | + ^ | [Process exited 0] | | | | - -- TERMINAL -- | + | ]]) end) end) diff --git a/test/functional/core/path_spec.lua b/test/functional/core/path_spec.lua new file mode 100644 index 0000000000..669bc99136 --- /dev/null +++ b/test/functional/core/path_spec.lua @@ -0,0 +1,56 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eq = helpers.eq +local eval = helpers.eval +local command = helpers.command +local iswin = helpers.iswin + +describe('path collapse', function() + local targetdir + local expected_path + + local function join_path(...) + local pathsep = (iswin() and '\\' or '/') + return table.concat({...}, pathsep) + end + + before_each(function() + targetdir = join_path('test', 'functional', 'fixtures') + clear() + command('edit '..join_path(targetdir, 'tty-test.c')) + expected_path = eval('expand("%:p")') + end) + + it('with /./ segment #7117', function() + command('edit '..join_path(targetdir, '.', 'tty-test.c')) + eq(expected_path, eval('expand("%:p")')) + end) + + it('with ./ prefix #7117', function() + command('edit '..join_path('.', targetdir, 'tty-test.c')) + eq(expected_path, eval('expand("%:p")')) + end) + + it('with ./ prefix, after directory change #7117', function() + command('edit '..join_path('.', targetdir, 'tty-test.c')) + command('cd test') + eq(expected_path, eval('expand("%:p")')) + end) + + it('with /../ segment #7117', function() + command('edit '..join_path(targetdir, '..', 'fixtures', 'tty-test.c')) + eq(expected_path, eval('expand("%:p")')) + end) + + it('with ../ and different starting directory #7117', function() + command('cd test') + command('edit '..join_path('..', targetdir, 'tty-test.c')) + eq(expected_path, eval('expand("%:p")')) + end) + + it('with ./../ and different starting directory #7117', function() + command('cd test') + command('edit '..join_path('.', '..', targetdir, 'tty-test.c')) + eq(expected_path, eval('expand("%:p")')) + end) +end) diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua new file mode 100644 index 0000000000..ae7f949e52 --- /dev/null +++ b/test/functional/core/startup_spec.lua @@ -0,0 +1,96 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') + +local clear = helpers.clear +local command = helpers.command +local eq = helpers.eq +local funcs = helpers.funcs +local nvim_prog = helpers.nvim_prog +local nvim_set = helpers.nvim_set +local read_file = helpers.read_file +local retry = helpers.retry +local iswin = helpers.iswin + +describe('startup', function() + before_each(function() + clear() + end) + after_each(function() + os.remove('Xtest_startup_ttyout') + end) + + it('pipe at both ends: has("ttyin")==0 has("ttyout")==0', function() + -- system() puts a pipe at both ends. + local out = funcs.system({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', nvim_set, + '-c', [[echo has('ttyin') has('ttyout')]], + '+q' }) + eq('0 0', out) + end) + it('with --embed: has("ttyin")==0 has("ttyout")==0', function() + local screen = Screen.new(25, 3) + -- Remote UI connected by --embed. + screen:attach() + command([[echo has('ttyin') has('ttyout')]]) + screen:expect([[ + ^ | + ~ | + 0 0 | + ]]) + end) + it('in a TTY: has("ttyin")==1 has("ttyout")==1', function() + local screen = Screen.new(25, 3) + screen:attach() + if iswin() then + command([[set shellcmdflag=/s\ /c shellxquote=\"]]) + end + -- Running in :terminal + command([[exe printf("terminal %s -u NONE -i NONE --cmd \"]] + ..nvim_set..[[\" ]] + ..[[-c \"echo has('ttyin') has('ttyout')\""]] + ..[[, shellescape(v:progpath))]]) + screen:expect([[ + ^ | + 1 1 | + | + ]]) + end) + it('output to pipe: has("ttyin")==1 has("ttyout")==0', function() + local screen = Screen.new(25, 5) + screen:attach() + if iswin() then + command([[set shellcmdflag=/s\ /c shellxquote=\"]]) + end + -- Running in :terminal + command([[exe printf("terminal %s -u NONE -i NONE --cmd \"]] + ..nvim_set..[[\" ]] + ..[[-c \"call writefile([has('ttyin'), has('ttyout')], 'Xtest_startup_ttyout')\"]] + ..[[-c q | cat -v"]] -- Output to a pipe. + ..[[, shellescape(v:progpath))]]) + retry(nil, 3000, function() + screen:sleep(1) + eq('1\n0\n', -- stdin is a TTY, stdout is a pipe + read_file('Xtest_startup_ttyout')) + end) + end) + it('input from pipe: has("ttyin")==0 has("ttyout")==1', function() + local screen = Screen.new(25, 5) + screen:attach() + if iswin() then + command([[set shellcmdflag=/s\ /c shellxquote=\"]]) + end + -- Running in :terminal + command([[exe printf("terminal echo foo | ]] -- Input from a pipe. + ..[[%s -u NONE -i NONE --cmd \"]] + ..nvim_set..[[\" ]] + ..[[-c \"call writefile([has('ttyin'), has('ttyout')], 'Xtest_startup_ttyout')\"]] + ..[[-c q -- -"]] + ..[[, shellescape(v:progpath))]]) + retry(nil, 3000, function() + screen:sleep(1) + eq('0\n1\n', -- stdin is a pipe, stdout is a TTY + read_file('Xtest_startup_ttyout')) + end) + end) +end) + |