termxx_spec.lua (13707B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 local uv = vim.uv 5 6 local clear, command, testprg = n.clear, n.command, n.testprg 7 local eval, eq, neq, retry = n.eval, t.eq, t.neq, t.retry 8 local exec_lua = n.exec_lua 9 local matches = t.matches 10 local ok = t.ok 11 local feed = n.feed 12 local api = n.api 13 local pcall_err = t.pcall_err 14 local assert_alive = n.assert_alive 15 local skip = t.skip 16 local is_os = t.is_os 17 18 describe('autocmd TermClose', function() 19 before_each(function() 20 clear() 21 api.nvim_set_option_value('shell', testprg('shell-test'), {}) 22 command('set shellcmdflag=EXE shellredir= shellpipe= shellquote= shellxquote=') 23 command('autocmd! nvim.terminal TermClose') 24 end) 25 26 local function test_termclose_delete_own_buf() 27 -- The terminal process needs to keep running so that TermClose isn't triggered immediately. 28 api.nvim_set_option_value('shell', string.format('"%s" INTERACT', testprg('shell-test')), {}) 29 command('terminal') 30 local termbuf = api.nvim_get_current_buf() 31 command(('autocmd TermClose * bdelete! %d'):format(termbuf)) 32 matches( 33 '^TermClose Autocommands for "%*": Vim%(bdelete%):E937: Attempt to delete a buffer that is in use: term://', 34 pcall_err(command, 'bdelete!') 35 ) 36 assert_alive() 37 end 38 39 it('TermClose deleting its own buffer, altbuf = buffer 1 #10386', function() 40 test_termclose_delete_own_buf() 41 end) 42 43 it('TermClose deleting its own buffer, altbuf NOT buffer 1 #10386', function() 44 command('edit foo1') 45 test_termclose_delete_own_buf() 46 end) 47 48 it('TermClose deleting all other buffers', function() 49 local oldbuf = api.nvim_get_current_buf() 50 -- The terminal process needs to keep running so that TermClose isn't triggered immediately. 51 api.nvim_set_option_value('shell', string.format('"%s" INTERACT', testprg('shell-test')), {}) 52 command(('autocmd TermClose * bdelete! %d'):format(oldbuf)) 53 command('horizontal terminal') 54 neq(oldbuf, api.nvim_get_current_buf()) 55 command('bdelete!') 56 feed('<C-G>') -- This shouldn't crash due to having a 0-line buffer. 57 assert_alive() 58 end) 59 60 it('TermClose switching back to terminal buffer', function() 61 local buf = api.nvim_get_current_buf() 62 api.nvim_open_term(buf, {}) 63 command(('autocmd TermClose * buffer %d | new'):format(buf)) 64 eq( 65 'TermClose Autocommands for "*": Vim(buffer):E1546: Cannot switch to a closing buffer', 66 pcall_err(command, 'bwipe!') 67 ) 68 assert_alive() 69 end) 70 71 it('triggers when fast-exiting terminal job stops', function() 72 command('autocmd TermClose * let g:test_termclose = 23') 73 command('terminal') 74 -- shell-test exits immediately. 75 retry(nil, nil, function() 76 neq(-1, eval('jobwait([&channel], 0)[0]')) 77 end) 78 retry(nil, nil, function() 79 eq(23, eval('g:test_termclose')) 80 end) 81 end) 82 83 it('triggers when long-running terminal job gets stopped', function() 84 api.nvim_set_option_value('shell', is_os('win') and 'cmd.exe' or 'sh', {}) 85 command('autocmd TermClose * let g:test_termclose = 23') 86 command('terminal') 87 command('call jobstop(b:terminal_job_id)') 88 retry(nil, nil, function() 89 eq(23, eval('g:test_termclose')) 90 end) 91 end) 92 93 it('kills job trapping SIGTERM', function() 94 skip(is_os('win'), 'N/A for Windows') 95 api.nvim_set_option_value('shell', 'sh', {}) 96 api.nvim_set_option_value('shellcmdflag', '-c', {}) 97 command( 98 [[ let g:test_job = jobstart('trap "" TERM && echo 1 && sleep 60', { ]] 99 .. [[ 'on_stdout': {-> execute('let g:test_job_started = 1')}, ]] 100 .. [[ 'on_exit': {-> execute('let g:test_job_exited = 1')}}) ]] 101 ) 102 retry(nil, nil, function() 103 eq(1, eval('get(g:, "test_job_started", 0)')) 104 end) 105 106 uv.update_time() 107 local start = uv.now() 108 command('call jobstop(g:test_job)') 109 retry(nil, nil, function() 110 eq(1, eval('get(g:, "test_job_exited", 0)')) 111 end) 112 uv.update_time() 113 local duration = uv.now() - start 114 -- Nvim begins SIGTERM after KILL_TIMEOUT_MS. 115 ok(duration >= 2000) 116 ok(duration <= 4000) -- Epsilon for slow CI 117 end) 118 119 it('kills PTY job trapping SIGHUP and SIGTERM', function() 120 skip(is_os('win'), 'N/A for Windows') 121 api.nvim_set_option_value('shell', 'sh', {}) 122 api.nvim_set_option_value('shellcmdflag', '-c', {}) 123 command( 124 [[ let g:test_job = jobstart('trap "" HUP TERM && echo 1 && sleep 60', { ]] 125 .. [[ 'pty': 1,]] 126 .. [[ 'on_stdout': {-> execute('let g:test_job_started = 1')}, ]] 127 .. [[ 'on_exit': {-> execute('let g:test_job_exited = 1')}}) ]] 128 ) 129 retry(nil, nil, function() 130 eq(1, eval('get(g:, "test_job_started", 0)')) 131 end) 132 133 uv.update_time() 134 local start = uv.now() 135 command('call jobstop(g:test_job)') 136 retry(nil, nil, function() 137 eq(1, eval('get(g:, "test_job_exited", 0)')) 138 end) 139 uv.update_time() 140 local duration = uv.now() - start 141 -- Nvim begins SIGKILL after (2 * KILL_TIMEOUT_MS). 142 ok(duration >= 4000) 143 ok(duration <= 7000) -- Epsilon for slow CI 144 end) 145 146 it('reports the correct <abuf>', function() 147 command('set hidden') 148 command('set shellcmdflag=EXE') 149 command('autocmd TermClose * let g:abuf = expand("<abuf>")') 150 command('edit foo') 151 command('edit bar') 152 eq(2, eval('bufnr("%")')) 153 154 command('terminal ls') 155 retry(nil, nil, function() 156 eq(3, eval('bufnr("%")')) 157 end) 158 159 command('buffer 1') 160 retry(nil, nil, function() 161 eq(1, eval('bufnr("%")')) 162 end) 163 164 command('3bdelete!') 165 retry(nil, nil, function() 166 eq('3', eval('g:abuf')) 167 end) 168 feed('<c-c>') 169 n.poke_eventloop() -- Wait for input to be flushed 170 n.expect_exit(1000, feed, ':qa!<cr>') 171 end) 172 173 it('exposes v:event.status', function() 174 command('set shellcmdflag=EXIT') 175 command('autocmd TermClose * let g:status = v:event.status') 176 177 command('terminal 0') 178 retry(nil, nil, function() 179 eq(0, eval('g:status')) 180 end) 181 182 command('terminal 42') 183 retry(nil, nil, function() 184 eq(42, eval('g:status')) 185 end) 186 187 command('set shellcmdflag= | terminal INTERACT') 188 retry(nil, nil, function() 189 matches('^interact %$ ?$', api.nvim_buf_get_lines(0, 0, 1, true)[1]) 190 end) 191 command('bwipe!') 192 eq(-1, eval('g:status')) 193 end) 194 end) 195 196 it('autocmd TermEnter, TermLeave', function() 197 clear() 198 command('let g:evs = []') 199 command('autocmd TermOpen * call add(g:evs, ["TermOpen", mode()])') 200 command('autocmd TermClose * call add(g:evs, ["TermClose", mode()])') 201 command('autocmd TermEnter * call add(g:evs, ["TermEnter", mode()])') 202 command('autocmd TermLeave * call add(g:evs, ["TermLeave", mode()])') 203 command('terminal') 204 205 feed('i') 206 eq({ { 'TermOpen', 'n' }, { 'TermEnter', 't' } }, eval('g:evs')) 207 feed([[<C-\><C-n>]]) 208 feed('A') 209 eq( 210 { { 'TermOpen', 'n' }, { 'TermEnter', 't' }, { 'TermLeave', 'n' }, { 'TermEnter', 't' } }, 211 eval('g:evs') 212 ) 213 214 -- TermLeave is also triggered by :quit. 215 command('split foo') 216 feed('<Ignore>') -- Add input to separate two RPC requests 217 command('wincmd w') 218 feed('i') 219 command('q!') 220 feed('<Ignore>') -- Add input to separate two RPC requests 221 eq({ 222 { 'TermOpen', 'n' }, 223 { 'TermEnter', 't' }, 224 { 'TermLeave', 'n' }, 225 { 'TermEnter', 't' }, 226 { 'TermLeave', 'n' }, 227 { 'TermEnter', 't' }, 228 { 'TermClose', 't' }, 229 { 'TermLeave', 'n' }, 230 }, eval('g:evs')) 231 end) 232 233 describe('autocmd TextChangedT,WinResized', function() 234 before_each(clear) 235 236 it('TextChangedT works', function() 237 local screen = Screen.new(50, 7) 238 screen:set_default_attr_ids({ 239 [1] = { bold = true }, 240 [31] = { foreground = Screen.colors.Gray100, background = Screen.colors.DarkGreen }, 241 [32] = { 242 foreground = Screen.colors.Gray100, 243 bold = true, 244 background = Screen.colors.DarkGreen, 245 }, 246 }) 247 248 local term, term_unfocused = exec_lua(function() 249 -- Split windows before opening terminals so TextChangedT doesn't fire an additional time due 250 -- to the inner terminal being resized (which is usually deferred too). 251 vim.cmd.vnew() 252 local term_unfocused = vim.api.nvim_open_term(0, {}) 253 vim.cmd.wincmd 'p' 254 local term = vim.api.nvim_open_term(0, {}) 255 vim.cmd.startinsert() 256 return term, term_unfocused 257 end) 258 eq('t', eval('mode()')) 259 260 exec_lua(function() 261 _G.n_triggered = 0 262 vim.api.nvim_create_autocmd('TextChanged', { 263 callback = function() 264 _G.n_triggered = _G.n_triggered + 1 265 end, 266 }) 267 _G.t_triggered = 0 268 vim.api.nvim_create_autocmd('TextChangedT', { 269 callback = function() 270 _G.t_triggered = _G.t_triggered + 1 271 end, 272 }) 273 end) 274 275 api.nvim_chan_send(term, 'a') 276 retry(nil, nil, function() 277 eq(1, exec_lua('return _G.t_triggered')) 278 end) 279 api.nvim_chan_send(term, 'b') 280 retry(nil, nil, function() 281 eq(2, exec_lua('return _G.t_triggered')) 282 end) 283 284 -- Not triggered by changes in a non-current terminal. 285 api.nvim_chan_send(term_unfocused, 'hello') 286 screen:expect([[ 287 hello │ab^ | 288 │ |*4 289 {31:[Scratch] [-] }{32:[Scratch] [-] }| 290 {1:-- TERMINAL --} | 291 ]]) 292 eq(2, exec_lua('return _G.t_triggered')) 293 294 -- Not triggered by unflushed redraws. 295 api.nvim__redraw({ valid = false, flush = false }) 296 eq(2, exec_lua('return _G.t_triggered')) 297 298 -- Not triggered when not in terminal mode. 299 command('stopinsert') 300 eq('n', eval('mode()')) 301 eq(2, exec_lua('return _G.t_triggered')) 302 eq(0, exec_lua('return _G.n_triggered')) -- Nothing we did was in Normal mode yet. 303 304 api.nvim_chan_send(term, 'c') 305 screen:expect([[ 306 hello │a^bc | 307 │ |*4 308 {31:[Scratch] [-] }{32:[Scratch] [-] }| 309 | 310 ]]) 311 eq(1, exec_lua('return _G.n_triggered')) -- Happened in Normal mode. 312 end) 313 314 it('no crash when deleting terminal buffer', function() 315 -- Using nvim_open_term over :terminal as the former can free the terminal immediately on 316 -- close, causing the crash. 317 318 -- WinResized 319 local buf1, term1 = exec_lua(function() 320 vim.cmd.new() 321 local buf = vim.api.nvim_get_current_buf() 322 local term = vim.api.nvim_open_term(0, { 323 on_input = function() 324 vim.cmd.wincmd '_' 325 end, 326 }) 327 vim.api.nvim_create_autocmd('WinResized', { 328 once = true, 329 command = 'bwipeout!', 330 }) 331 return buf, term 332 end) 333 feed('ii') 334 eq(false, api.nvim_buf_is_valid(buf1)) 335 eq('n', eval('mode()')) 336 eq({}, api.nvim_get_chan_info(term1)) -- Channel should've been cleaned up. 337 338 -- TextChangedT 339 local buf2, term2 = exec_lua(function() 340 vim.cmd.new() 341 local buf = vim.api.nvim_get_current_buf() 342 local term = vim.api.nvim_open_term(0, { 343 on_input = function(_, chan) 344 vim.api.nvim_chan_send(chan, 'sup') 345 end, 346 }) 347 vim.api.nvim_create_autocmd('TextChangedT', { 348 once = true, 349 command = 'bwipeout!', 350 }) 351 return buf, term 352 end) 353 feed('ii') 354 -- refresh_terminal is deferred, so TextChangedT may not trigger immediately. 355 retry(nil, nil, function() 356 eq(false, api.nvim_buf_is_valid(buf2)) 357 end) 358 eq('n', eval('mode()')) 359 eq({}, api.nvim_get_chan_info(term2)) -- Channel should've been cleaned up. 360 end) 361 end) 362 363 describe('no crash if :bwipe from TermClose is processed by', function() 364 local oldwin --- @type integer 365 local chan --- @type integer 366 367 before_each(function() 368 clear() 369 command('autocmd! nvim.terminal') 370 oldwin = api.nvim_get_current_win() 371 command('new') 372 local buf = api.nvim_get_current_buf() 373 chan = api.nvim_open_term(buf, {}) 374 api.nvim_set_var('chan', chan) 375 command(('autocmd TermClose <buffer> bwipe! %d'):format(buf)) 376 command('let g:done = 0') 377 feed('i') 378 eq({ mode = 't', blocking = false }, api.nvim_get_mode()) 379 end) 380 381 --- @param event string Event name. 382 --- @param trigger_cmd string The Ex command to trigger the event. 383 local function test_case(event, trigger_cmd) 384 api.nvim_create_autocmd( 385 event, 386 { nested = true, once = true, command = 'sleep 40m | let g:done = 1' } 387 ) 388 exec_lua(function() 389 vim.cmd(trigger_cmd) 390 vim.defer_fn(function() 391 vim.fn.chanclose(chan) 392 end, 25) 393 end) 394 retry(nil, 1000, function() 395 eq(1, api.nvim_get_var('done')) 396 end) 397 assert_alive() 398 eq({ mode = 'n', blocking = false }, api.nvim_get_mode()) 399 eq({ oldwin }, api.nvim_list_wins()) 400 feed('<Ignore>') -- Add input to separate two RPC requests. 401 -- Channel should have been released. 402 eq({}, api.nvim_get_chan_info(chan)) 403 end 404 405 it('WinResized autocommand in Terminal mode', function() 406 test_case('WinResized', 'vsplit') 407 end) 408 409 it('TextChangedT autocommand in Terminal mode', function() 410 test_case('TextChangedT', [[call chansend(g:chan, "foo\r\nbar")]]) 411 end) 412 413 it('TermRequest autocommand in Terminal mode', function() 414 test_case('TermRequest', [[call chansend(g:chan, "\x1b]11;?\x1b\\")]]) 415 end) 416 417 it('TermRequest autocommand in Normal mode', function() 418 feed([[<C-\><C-N>]]) 419 eq({ mode = 'nt', blocking = false }, api.nvim_get_mode()) 420 test_case('TermRequest', [[call chansend(g:chan, "\x1b]11;?\x1b\\")]]) 421 end) 422 end)