channel_spec.lua (11976B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 5 local clear = n.clear 6 local eq = t.eq 7 local eval = n.eval 8 local command = n.command 9 local pcall_err = t.pcall_err 10 local feed = n.feed 11 local poke_eventloop = n.poke_eventloop 12 local is_os = t.is_os 13 local api = n.api 14 local async_meths = n.async_meths 15 local testprg = n.testprg 16 local assert_alive = n.assert_alive 17 18 describe('terminal channel is closed and later released if', function() 19 local screen 20 21 before_each(function() 22 clear() 23 screen = Screen.new() 24 end) 25 26 it('opened by nvim_open_term() and deleted by :bdelete!', function() 27 command([[let id = nvim_open_term(0, {})]]) 28 local chans = eval('len(nvim_list_chans())') 29 -- Channel hasn't been released yet. 30 eq( 31 "Vim(call):Can't send data to closed stream", 32 pcall_err(command, [[bdelete! | call chansend(id, 'test')]]) 33 ) 34 -- Channel has been released after processing free_channel_event(). 35 eq(chans - 1, eval('len(nvim_list_chans())')) 36 37 command('autocmd BufWipeout * ++once let id2 = nvim_open_term(str2nr(expand("<abuf>")), {})') 38 -- Channel hasn't been released yet. 39 eq( 40 "Vim(call):Can't send data to closed stream", 41 pcall_err(command, [[bdelete! | call chansend(id2, 'test')]]) 42 ) 43 -- Channel has been released after processing free_channel_event(). 44 eq(chans - 1, eval('len(nvim_list_chans())')) 45 end) 46 47 it('opened by nvim_open_term(), closed by chanclose(), and deleted by pressing a key', function() 48 command('let id = nvim_open_term(0, {})') 49 local chans = eval('len(nvim_list_chans())') 50 -- Channel has been closed but not released. 51 eq( 52 "Vim(call):Can't send data to closed stream", 53 pcall_err(command, [[call chanclose(id) | call chansend(id, 'test')]]) 54 ) 55 screen:expect({ any = '%[Terminal closed%]' }) 56 eq(chans, eval('len(nvim_list_chans())')) 57 feed('i<CR>') -- Delete terminal. 58 poke_eventloop() -- Process pending input. 59 -- Channel has been released after processing free_channel_event(). 60 eq(chans - 1, eval('len(nvim_list_chans())')) 61 end) 62 63 it('opened by nvim_open_term(), closed by chanclose(), and deleted by :bdelete', function() 64 command('let id = nvim_open_term(0, {})') 65 local chans = eval('len(nvim_list_chans())') 66 -- Channel has been closed but not released. 67 eq( 68 "Vim(call):Can't send data to closed stream", 69 pcall_err(command, [[call chanclose(id) | call chansend(id, 'test')]]) 70 ) 71 screen:expect({ any = '%[Terminal closed%]' }) 72 eq(chans, eval('len(nvim_list_chans())')) 73 -- Channel still hasn't been released yet. 74 eq( 75 "Vim(call):Can't send data to closed stream", 76 pcall_err(command, [[bdelete | call chansend(id, 'test')]]) 77 ) 78 -- Channel has been released after processing free_channel_event(). 79 eq(chans - 1, eval('len(nvim_list_chans())')) 80 end) 81 82 it('opened by jobstart(…,{term=true}), exited, and deleted by pressing a key', function() 83 command([[let id = jobstart('echo',{'term':v:true})]]) 84 local chans = eval('len(nvim_list_chans())') 85 -- Wait for process to exit. 86 screen:expect({ any = '%[Process exited 0%]' }) 87 -- Process has exited but channel has't been released. 88 eq( 89 "Vim(call):Can't send data to closed stream", 90 pcall_err(command, [[call chansend(id, 'test')]]) 91 ) 92 eq(chans, eval('len(nvim_list_chans())')) 93 feed('i<CR>') -- Delete terminal. 94 poke_eventloop() -- Process pending input. 95 poke_eventloop() -- Process term_delayed_free(). 96 -- Channel has been released after processing free_channel_event(). 97 eq(chans - 1, eval('len(nvim_list_chans())')) 98 end) 99 100 -- This indirectly covers #16264 101 it('opened by jobstart(…,{term=true}), exited, and deleted by :bdelete', function() 102 command([[let id = jobstart('echo', {'term':v:true})]]) 103 local chans = eval('len(nvim_list_chans())') 104 -- Wait for process to exit. 105 screen:expect({ any = '%[Process exited 0%]' }) 106 -- Process has exited but channel hasn't been released. 107 eq( 108 "Vim(call):Can't send data to closed stream", 109 pcall_err(command, [[call chansend(id, 'test')]]) 110 ) 111 eq(chans, eval('len(nvim_list_chans())')) 112 -- Channel still hasn't been released yet. 113 eq( 114 "Vim(call):Can't send data to closed stream", 115 pcall_err(command, [[bdelete | call chansend(id, 'test')]]) 116 ) 117 poke_eventloop() -- Process term_delayed_free(). 118 -- Channel has been released after processing free_channel_event(). 119 eq(chans - 1, eval('len(nvim_list_chans())')) 120 end) 121 end) 122 123 it('chansend sends lines to terminal channel in proper order', function() 124 clear({ args = { '--cmd', 'set laststatus=2' } }) 125 local screen = Screen.new(100, 20) 126 screen._default_attr_ids = nil 127 local shells = is_os('win') and { 'cmd.exe', 'pwsh.exe -nop', 'powershell.exe -nop' } or { 'sh' } 128 for _, sh in ipairs(shells) do 129 command([[let id = jobstart(']] .. sh .. [[', {'term':v:true})]]) 130 -- On Windows this may fail if the shell hasn't fully started yet, so retry. 131 t.retry(is_os('win') and 3 or 1, 5000, function() 132 command([[call chansend(id, ['echo "hello"', 'echo "world"', ''])]]) 133 -- With PowerShell the command may be highlighted, so specify attr_ids = {}. 134 screen:expect { any = [[echo "hello".*echo "world"]], attr_ids = {}, timeout = 2000 } 135 end) 136 command('bdelete!') 137 screen:expect { any = '%[No Name%]' } 138 end 139 end) 140 141 --- @param event string 142 --- @param extra_tests fun(table, table)? 143 local function test_autocmd_no_crash(event, extra_tests) 144 local env = {} 145 -- Use REPFAST for immediately output after start. 146 local term_args = { testprg('shell-test'), 'REPFAST', '50', 'TEST' } 147 148 before_each(function() 149 clear() 150 env.screen = Screen.new(60, 4) 151 command([[file Xoldbuf | call setline(1, 'OLDBUF') | enew]]) 152 -- Wait before :bwipe to avoid closing PTY master before the child calls setsid(), 153 -- as that will cause SIGHUP to be also sent to the parent. 154 -- Use vim.uv.sleep() which blocks the event loop. 155 n.exec([[ 156 func Wipe() 157 lua vim.uv.sleep(5) 158 bwipe! 159 endfunc 160 ]]) 161 end) 162 163 local input_prompt_screen = [[ 164 | 165 {1:~ }|*2 166 ^ | 167 ]] 168 local oldbuf_screen = [[ 169 ^OLDBUF | 170 {1:~ }|*2 171 | 172 ]] 173 174 it('processes job exit event when using jobstart(…,{term=true})', function() 175 api.nvim_create_autocmd(event, { command = "call input('')" }) 176 async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) 177 env.screen:expect(input_prompt_screen) 178 vim.uv.sleep(20) 179 feed('<CR>') 180 env.screen:expect([[ 181 ^0: TEST | 182 1: TEST | 183 2: TEST | 184 | 185 ]]) 186 feed('i') 187 env.screen:expect([[ 188 49: TEST | 189 | 190 [Process exited 0]^ | 191 {5:-- TERMINAL --} | 192 ]]) 193 feed('<CR>') 194 env.screen:expect(oldbuf_screen) 195 assert_alive() 196 end) 197 198 it('wipes buffer and processes events when using jobstart(…,{term=true})', function() 199 api.nvim_create_autocmd(event, { command = "call Wipe() | call input('')" }) 200 async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) 201 env.screen:expect(input_prompt_screen) 202 vim.uv.sleep(20) 203 feed('<CR>') 204 env.screen:expect(oldbuf_screen) 205 assert_alive() 206 eq('Xoldbuf', eval('bufname()')) 207 eq(0, eval([[exists('b:term_title')]])) 208 end) 209 210 it('processes :bwipe from TermClose when using jobstart(…,{term=true})', function() 211 local term_buf = api.nvim_get_current_buf() 212 api.nvim_create_autocmd('TermClose', { command = ('bwipe! %d'):format(term_buf) }) 213 api.nvim_create_autocmd(event, { command = "call input('')", nested = true }) 214 async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) 215 env.screen:expect(input_prompt_screen) 216 vim.uv.sleep(20) 217 feed('<CR>') 218 env.screen:expect(oldbuf_screen) 219 assert_alive() 220 eq('Xoldbuf', eval('bufname()')) 221 eq(0, eval([[exists('b:term_title')]])) 222 end) 223 224 it('only wipes buffer when using jobstart(…,{term=true})', function() 225 api.nvim_create_autocmd(event, { command = 'call Wipe()' }) 226 async_meths.nvim_call_function('jobstart', { term_args, { term = true } }) 227 env.screen:expect(oldbuf_screen) 228 assert_alive() 229 eq('Xoldbuf', eval('bufname()')) 230 eq(0, eval([[exists('b:term_title')]])) 231 end) 232 233 if extra_tests then 234 extra_tests(env, term_args) 235 end 236 end 237 238 describe('no crash when TermOpen autocommand', function() 239 test_autocmd_no_crash('TermOpen', function(env, term_args) 240 it('wipes buffer and processes events when using nvim_open_term()', function() 241 api.nvim_create_autocmd('TermOpen', { command = "call Wipe() | call input('')" }) 242 async_meths.nvim_open_term(0, {}) 243 env.screen:expect([[ 244 | 245 {1:~ }|*2 246 ^ | 247 ]]) 248 feed('<CR>') 249 env.screen:expect([[ 250 ^OLDBUF | 251 {1:~ }|*2 252 | 253 ]]) 254 assert_alive() 255 end) 256 257 it('wipes buffer when using jobstart(…,{term=true}) during Nvim exit', function() 258 n.expect_exit(n.exec_lua, function() 259 vim.schedule(function() 260 vim.fn.jobstart(term_args, { term = true }) 261 end) 262 vim.api.nvim_create_autocmd('TermOpen', { command = 'call Wipe()' }) 263 vim.cmd('qall!') 264 end) 265 end) 266 end) 267 end) 268 269 describe('no crash when BufFilePre autocommand', function() 270 test_autocmd_no_crash('BufFilePre') 271 end) 272 273 describe('no crash when BufFilePost autocommand', function() 274 test_autocmd_no_crash('BufFilePost') 275 end) 276 277 describe('nvim_open_term', function() 278 local screen 279 280 before_each(function() 281 clear() 282 screen = Screen.new(8, 10) 283 end) 284 285 it('with force_crlf=true converts newlines', function() 286 local win = api.nvim_get_current_win() 287 local buf = api.nvim_create_buf(false, true) 288 local term = api.nvim_open_term(buf, { force_crlf = true }) 289 api.nvim_win_set_buf(win, buf) 290 api.nvim_chan_send(term, 'here\nthere\nfoo\r\nbar\n\ntest') 291 screen:expect([[ 292 ^here | 293 there | 294 foo | 295 bar | 296 | 297 test | 298 |*4 299 ]]) 300 api.nvim_chan_send(term, '\nfirst') 301 screen:expect([[ 302 ^here | 303 there | 304 foo | 305 bar | 306 | 307 test | 308 first | 309 |*3 310 ]]) 311 end) 312 313 it('with force_crlf=false does not convert newlines', function() 314 local win = api.nvim_get_current_win() 315 local buf = api.nvim_create_buf(false, true) 316 local term = api.nvim_open_term(buf, { force_crlf = false }) 317 api.nvim_win_set_buf(win, buf) 318 api.nvim_chan_send(term, 'here\nthere') 319 screen:expect([[ 320 ^here | 321 there | 322 |*8 323 ]]) 324 end) 325 end)