ex_terminal_spec.lua (12343B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 5 local assert_alive = n.assert_alive 6 local clear, poke_eventloop = n.clear, n.poke_eventloop 7 local testprg, source, eq, neq = n.testprg, n.source, t.eq, t.neq 8 local feed = n.feed 9 local eval = n.eval 10 local fn = n.fn 11 local api = n.api 12 local exec_lua = n.exec_lua 13 local retry = t.retry 14 local ok = t.ok 15 local command = n.command 16 local skip = t.skip 17 local is_os = t.is_os 18 local is_ci = t.is_ci 19 20 describe(':terminal', function() 21 local screen 22 23 before_each(function() 24 clear() 25 screen = Screen.new(50, 4, { rgb = false }) 26 screen._default_attr_ids = nil 27 end) 28 29 it('does not interrupt Press-ENTER prompt #2748', function() 30 -- Ensure that :messages shows Press-ENTER. 31 source([[ 32 echomsg "msg1" 33 echomsg "msg2" 34 echomsg "msg3" 35 ]]) 36 -- Invoke a command that emits frequent terminal activity. 37 feed([[:terminal "]] .. testprg('shell-test') .. [[" REP 9999 !terminal_output!<cr>]]) 38 feed([[<C-\><C-N>]]) 39 poke_eventloop() 40 -- Wait for some terminal activity. 41 retry(nil, 4000, function() 42 ok(fn.line('$') > 6) 43 end) 44 feed(':messages<CR>') 45 screen:expect([[ 46 msg1 | 47 msg2 | 48 msg3 | 49 Press ENTER or type command to continue^ | 50 ]]) 51 end) 52 53 it('reads output buffer on terminal reporting #4151', function() 54 skip(is_ci('cirrus') or is_os('win')) 55 if is_os('win') then 56 command( 57 [[terminal powershell -NoProfile -NoLogo -Command Write-Host -NoNewline "\"$([char]27)[6n\""; Start-Sleep -Milliseconds 500 ]] 58 ) 59 else 60 command([[terminal printf '\e[6n'; sleep 0.5 ]]) 61 end 62 screen:expect { any = '%^%[%[1;1R' } 63 end) 64 65 it('in normal-mode :split does not move cursor', function() 66 if is_os('win') then 67 command( 68 [[terminal for /L \\%I in (1,0,2) do ( echo foo & ping -w 100 -n 1 127.0.0.1 > nul )]] 69 ) 70 else 71 command([[terminal while true; do echo foo; sleep .1; done]]) 72 end 73 feed([[<C-\><C-N>M]]) -- move cursor away from last line 74 poke_eventloop() 75 eq(3, eval("line('$')")) -- window height 76 eq(2, eval("line('.')")) -- cursor is in the middle 77 feed(':vsplit<CR>') 78 eq(2, eval("line('.')")) -- cursor stays where we put it 79 feed(':split<CR>') 80 eq(2, eval("line('.')")) -- cursor stays where we put it 81 end) 82 83 it('Enter/Leave does not increment jumplist #3723', function() 84 feed(':terminal<CR>') 85 local function enter_and_leave() 86 local lines_before = fn.line('$') 87 -- Create a new line (in the shell). For a normal buffer this 88 -- increments the jumplist; for a terminal-buffer it should not. #3723 89 feed('i') 90 poke_eventloop() 91 feed('<CR><CR><CR><CR>') 92 poke_eventloop() 93 feed([[<C-\><C-N>]]) 94 poke_eventloop() 95 -- Wait for >=1 lines to be created. 96 retry(nil, 4000, function() 97 ok(fn.line('$') > lines_before) 98 end) 99 end 100 enter_and_leave() 101 enter_and_leave() 102 enter_and_leave() 103 ok(fn.line('$') > 6) -- Verify assumption. 104 local jumps = fn.split(fn.execute('jumps'), '\n') 105 eq(' jump line col file/text', jumps[1]) 106 eq(3, #jumps) 107 end) 108 109 it('nvim_get_mode() in :terminal', function() 110 command('terminal') 111 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 112 feed('i') 113 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 114 feed([[<C-\><C-N>]]) 115 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 116 end) 117 118 it(':stopinsert RPC request exits terminal-mode #7807', function() 119 command('terminal') 120 feed('i[tui] insert-mode') 121 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 122 command('stopinsert') 123 feed('<Ignore>') -- Add input to separate two RPC requests 124 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 125 end) 126 127 it(":stopinsert in normal mode doesn't break insert mode #9889", function() 128 command('terminal') 129 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 130 command('stopinsert') 131 feed('<Ignore>') -- Add input to separate two RPC requests 132 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 133 feed('a') 134 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 135 end) 136 137 it('switching to terminal buffer in Insert mode goes to Terminal mode #7164', function() 138 command('terminal') 139 command('vnew') 140 feed('i') 141 command('let g:events = []') 142 command('autocmd InsertLeave * let g:events += ["InsertLeave"]') 143 command('autocmd TermEnter * let g:events += ["TermEnter"]') 144 command('inoremap <F2> <Cmd>wincmd p<CR>') 145 eq({ blocking = false, mode = 'i' }, api.nvim_get_mode()) 146 feed('<F2>') 147 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 148 eq({ 'InsertLeave', 'TermEnter' }, eval('g:events')) 149 end) 150 151 it('switching to terminal buffer immediately after :stopinsert #27031', function() 152 command('terminal') 153 command('vnew') 154 feed('i') 155 eq({ blocking = false, mode = 'i' }, api.nvim_get_mode()) 156 command('stopinsert | wincmd p') 157 feed('<Ignore>') -- Add input to separate two RPC requests 158 eq({ blocking = false, mode = 'nt' }, api.nvim_get_mode()) 159 end) 160 161 it('switching to another terminal buffer in Terminal mode', function() 162 command('terminal') 163 local buf0 = api.nvim_get_current_buf() 164 command('terminal') 165 local buf1 = api.nvim_get_current_buf() 166 command('terminal') 167 local buf2 = api.nvim_get_current_buf() 168 neq(buf0, buf1) 169 neq(buf0, buf2) 170 neq(buf1, buf2) 171 feed('i') 172 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 173 api.nvim_set_current_buf(buf1) 174 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 175 api.nvim_set_current_buf(buf0) 176 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 177 exec_lua(function() 178 vim.api.nvim_set_current_buf(buf1) 179 vim.api.nvim_buf_delete(buf0, { force = true }) 180 end) 181 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 182 api.nvim_set_current_buf(buf2) 183 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 184 api.nvim_set_current_buf(buf1) 185 eq({ blocking = false, mode = 't' }, api.nvim_get_mode()) 186 end) 187 end) 188 189 local function test_terminal_with_fake_shell(backslash) 190 -- shell-test.c is a fake shell that prints its arguments and exits. 191 local shell_path = testprg('shell-test') 192 if backslash then 193 shell_path = shell_path:gsub('/', [[\]]) 194 end 195 196 local screen 197 198 before_each(function() 199 clear() 200 screen = Screen.new(50, 4, { rgb = false }) 201 screen._default_attr_ids = nil 202 api.nvim_set_option_value('shell', shell_path, {}) 203 api.nvim_set_option_value('shellcmdflag', 'EXE', {}) 204 api.nvim_set_option_value('shellxquote', '', {}) -- win: avoid extra quotes 205 t.mkdir('Xsomedir') 206 t.write_file('Xsomedir/Xuniquefile', '') 207 end) 208 209 after_each(function() 210 n.rmdir('Xsomedir') 211 end) 212 213 it('with no argument, acts like jobstart(…,{term=true})', function() 214 command('autocmd! nvim.terminal TermClose') 215 command('terminal') 216 screen:expect([[ 217 ^ready $ | 218 [Process exited 0] | 219 |*2 220 ]]) 221 end) 222 223 it("with no argument, and 'shell' is set to empty string", function() 224 api.nvim_set_option_value('shell', '', {}) 225 eq("Vim(terminal):E91: 'shell' option is empty", t.pcall_err(command, 'terminal')) 226 end) 227 228 it("with no argument, but 'shell' has arguments, acts like jobstart(…,{term=true})", function() 229 api.nvim_set_option_value('shell', shell_path .. ' INTERACT', {}) 230 command('terminal') 231 screen:expect([[ 232 ^interact $ | 233 |*3 234 ]]) 235 end) 236 237 it('executes a given command through the shell', function() 238 command('terminal echo hi') 239 screen:expect([[ 240 ^ready $ echo hi | 241 | 242 [Process exited 0] | 243 | 244 ]]) 245 end) 246 247 it("executes a given command through the shell, when 'shell' has arguments", function() 248 api.nvim_set_option_value('shell', shell_path .. ' -t jeff', {}) 249 command('terminal echo hi') 250 screen:expect([[ 251 ^jeff $ echo hi | 252 | 253 [Process exited 0] | 254 | 255 ]]) 256 end) 257 258 it('allows quotes and slashes', function() 259 command([[terminal echo 'hello' \ "world"]]) 260 screen:expect([[ 261 ^ready $ echo 'hello' \ "world" | 262 | 263 [Process exited 0] | 264 | 265 ]]) 266 end) 267 268 it('ex_terminal() double-free #4554', function() 269 source([[ 270 autocmd BufNew * set shell=foo 271 terminal]]) 272 -- Verify that BufNew actually fired (else the test is invalid). 273 eq('foo', eval('&shell')) 274 end) 275 276 it('ignores writes if the backing stream closes', function() 277 command('autocmd! nvim.terminal TermClose') 278 command('terminal') 279 feed('iiXXXXXXX') 280 poke_eventloop() 281 -- Race: Though the shell exited (and streams were closed by SIGCHLD 282 -- handler), :terminal cleanup is pending on the main-loop. 283 -- This write should be ignored (not crash, #5445). 284 feed('iiYYYYYYY') 285 assert_alive() 286 end) 287 288 it('works with findfile()', function() 289 command('autocmd! nvim.terminal TermClose') 290 command('terminal') 291 eq('term://', string.match(eval('bufname("%")'), '^term://')) 292 eq('Xsomedir/Xuniquefile', eval('findfile("Xsomedir/Xuniquefile", ".")')) 293 end) 294 295 it('works with :find', function() 296 command('autocmd! nvim.terminal TermClose') 297 command('terminal') 298 screen:expect([[ 299 ^ready $ | 300 [Process exited 0] | 301 |*2 302 ]]) 303 eq('term://', string.match(eval('bufname("%")'), '^term://')) 304 feed([[<C-\><C-N>]]) 305 command([[find */Xuniquefile]]) 306 if is_os('win') then 307 eq('Xsomedir\\Xuniquefile', eval('bufname("%")')) 308 else 309 eq('Xsomedir/Xuniquefile', eval('bufname("%")')) 310 end 311 end) 312 313 it('works with gf', function() 314 command([[terminal echo "Xsomedir/Xuniquefile"]]) 315 screen:expect([[ 316 ^ready $ echo "Xsomedir/Xuniquefile" | 317 | 318 [Process exited 0] | 319 | 320 ]]) 321 feed([[<C-\><C-N>]]) 322 eq('term://', string.match(eval('bufname("%")'), '^term://')) 323 feed([[ggf"lgf]]) 324 eq('Xsomedir/Xuniquefile', eval('bufname("%")')) 325 end) 326 327 it('with bufhidden=delete #3958', function() 328 command('set hidden') 329 eq(1, eval('&hidden')) 330 command('autocmd BufNew * setlocal bufhidden=delete') 331 for _ = 1, 5 do 332 source([[ 333 execute 'edit '.reltimestr(reltime()) 334 terminal]]) 335 end 336 end) 337 338 describe('exit does not have long delay #27615', function() 339 for _, ut in ipairs({ 5, 50, 500, 5000, 50000, 500000 }) do 340 it(('with updatetime=%d'):format(ut), function() 341 api.nvim_set_option_value('updatetime', ut, {}) 342 api.nvim_set_option_value('shellcmdflag', 'EXIT', {}) 343 command('terminal 42') 344 screen:expect([[ 345 ^ | 346 [Process exited 42] | 347 |*2 348 ]]) 349 end) 350 end 351 end) 352 end 353 354 describe(':terminal (with fake shell)', function() 355 test_terminal_with_fake_shell(false) 356 if is_os('win') then 357 describe("when 'shell' uses backslashes", function() 358 test_terminal_with_fake_shell(true) 359 end) 360 end 361 end)