swapfile_preserve_recover_spec.lua (20377B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 5 local uv = vim.uv 6 local eq, eval, expect, exec = t.eq, n.eval, n.expect, n.exec 7 local assert_alive = n.assert_alive 8 local clear = n.clear 9 local command = n.command 10 local feed = n.feed 11 local fn = n.fn 12 local neq = t.neq 13 local nvim_prog = n.nvim_prog 14 local ok = t.ok 15 local rmdir = n.rmdir 16 local new_pipename = n.new_pipename 17 local pesc = vim.pesc 18 local set_session = n.set_session 19 local async_meths = n.async_meths 20 local expect_msg_seq = n.expect_msg_seq 21 local pcall_err = t.pcall_err 22 local mkdir = t.mkdir 23 local poke_eventloop = n.poke_eventloop 24 local api = n.api 25 local retry = t.retry 26 local write_file = t.write_file 27 28 describe(':recover', function() 29 before_each(clear) 30 31 it('fails if given a non-existent swapfile', function() 32 local swapname = 'bogus_swapfile' 33 local swapname2 = 'bogus_swapfile.swp' 34 eq( 35 'Vim(recover):E305: No swap file found for ' .. swapname, 36 pcall_err(command, 'recover ' .. swapname) 37 ) -- Should not segfault. #2117 38 -- Also check filename ending with ".swp". #9504 39 eq('Vim(recover):E306: Cannot open ' .. swapname2, pcall_err(command, 'recover ' .. swapname2)) -- Should not segfault. #2117 40 assert_alive() 41 end) 42 end) 43 44 describe("preserve and (R)ecover with custom 'directory'", function() 45 local swapdir = uv.cwd() .. '/Xtest_recover_dir' 46 local testfile = 'Xtest_recover_file1' 47 -- Put swapdir at the start of the 'directory' list. #1836 48 -- Note: `set swapfile` *must* go after `set directory`: otherwise it may 49 -- attempt to create a swapfile in different directory. 50 local init = [[ 51 set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[// 52 set swapfile fileformat=unix undolevels=-1 53 ]] 54 55 local nvim0 --- @type test.Session 56 before_each(function() 57 nvim0 = n.new_session(false) 58 set_session(nvim0) 59 rmdir(swapdir) 60 mkdir(swapdir) 61 end) 62 after_each(function() 63 command('%bwipeout!') 64 rmdir(swapdir) 65 end) 66 67 --- @return string 68 local function setup_swapname() 69 exec(init) 70 command('edit! ' .. testfile) 71 feed('isometext<esc>') 72 exec('redir => g:swapname | silent swapname | redir END') 73 return eval('g:swapname'):match('[^\n]*$') 74 end 75 76 local function test_recover(swappath1) 77 -- Start another Nvim instance. 78 local nvim2 = 79 n.new_session(false, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false }) 80 set_session(nvim2) 81 82 exec(init) 83 84 -- Use the "SwapExists" event to choose the (R)ecover choice at the dialog. 85 command('autocmd SwapExists * let v:swapchoice = "r"') 86 command('silent edit! ' .. testfile) 87 exec('redir => g:swapname | silent swapname | redir END') 88 89 local swappath2 = eval('g:swapname') 90 91 expect('sometext') 92 -- swapfile from session 1 should end in .swp 93 eq(testfile .. '.swp', string.match(swappath1, '[^%%]+$')) 94 -- swapfile from session 2 should end in .swo 95 eq(testfile .. '.swo', string.match(swappath2, '[^%%]+$')) 96 -- Verify that :swapname was not truncated (:help 'shortmess'). 97 ok(nil == string.find(swappath1, '%.%.%.')) 98 ok(nil == string.find(swappath2, '%.%.%.')) 99 end 100 101 it('with :preserve and SIGKILL', function() 102 local swappath1 = setup_swapname() 103 command('preserve') 104 neq(nil, uv.fs_stat(swappath1)) 105 eq(0, vim.uv.kill(eval('getpid()'), 'sigkill')) 106 test_recover(swappath1) 107 end) 108 109 it('closing stdio channel without :preserve #22096', function() 110 local swappath1 = setup_swapname() 111 nvim0:close() 112 neq(nil, uv.fs_stat(swappath1)) 113 test_recover(swappath1) 114 end) 115 116 it('killing TUI process without :preserve #22096', function() 117 local screen0 = Screen.new() 118 local child_server = new_pipename() 119 fn.jobstart({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--listen', child_server }, { 120 term = true, 121 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 122 }) 123 screen0:expect({ any = pesc('[No Name]') }) -- Wait for the child process to start. 124 local child_session = n.connect(child_server) 125 set_session(child_session) 126 local swappath1 = setup_swapname() 127 set_session(nvim0) 128 -- n.exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]]) 129 command('call chanclose(&channel)') -- Kill the child process. 130 screen0:expect({ any = pesc('[Process exited 1]') }) -- Wait for the child process to stop. 131 neq(nil, uv.fs_stat(swappath1)) 132 test_recover(swappath1) 133 end) 134 135 it('manual :recover with multiple swapfiles', function() 136 local swappath1 = setup_swapname() 137 eq('.swp', swappath1:match('%.[^.]+$')) 138 nvim0:close() 139 neq(nil, uv.fs_stat(swappath1)) 140 local swappath2 = swappath1:gsub('%.swp$', '.swo') 141 eq(true, uv.fs_copyfile(swappath1, swappath2)) 142 clear() 143 exec(init) 144 local screen = Screen.new(256, 40) 145 feed(':recover! ' .. testfile .. '<CR>') 146 screen:expect({ 147 any = { 148 '\nSwap files found:', 149 '\n In directory ', 150 vim.pesc('\n1. '), 151 vim.pesc('\n2. '), 152 vim.pesc('\nEnter number of swap file to use (0 to quit): ^'), 153 }, 154 none = vim.pesc('{18:^@}'), 155 }) 156 feed('2<CR>') 157 screen:expect({ 158 any = { 159 vim.pesc('\nRecovery completed.'), 160 vim.pesc('\n{6:Press ENTER or type command to continue}^'), 161 }, 162 }) 163 feed('<CR>') 164 expect('sometext') 165 end) 166 end) 167 168 describe('swapfile detection', function() 169 local swapdir = uv.cwd() .. '/Xtest_swapdialog_dir' 170 local nvim0 --- @type test.Session 171 -- Put swapdir at the start of the 'directory' list. #1836 172 -- Note: `set swapfile` *must* go after `set directory`: otherwise it may 173 -- attempt to create a swapfile in different directory. 174 local init = [[ 175 set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[// 176 set swapfile fileformat=unix nomodified undolevels=-1 nohidden 177 ]] 178 before_each(function() 179 nvim0 = n.new_session(false) 180 set_session(nvim0) 181 rmdir(swapdir) 182 mkdir(swapdir) 183 end) 184 after_each(function() 185 set_session(nvim0) 186 command('%bwipeout!') 187 rmdir(swapdir) 188 end) 189 190 it('redrawing during prompt does not break treesitter', function() 191 local testfile = 'Xtest_swapredraw.lua' 192 finally(function() 193 os.remove(testfile) 194 end) 195 write_file( 196 testfile, 197 [[ 198 vim.o.foldmethod = 'expr' 199 vim.o.foldexpr = 'v:lua.vim.treesitter.foldexpr()' 200 vim.defer_fn(function() 201 vim.api.nvim__redraw({ valid = false }) 202 end, 500) 203 pcall(vim.cmd.edit, 'Xtest_swapredraw.lua') 204 ]] 205 ) 206 exec(init) 207 command('edit! ' .. testfile) 208 command('preserve') 209 local args2 = { '--clean', '--embed', '--cmd', n.runtime_set } 210 local nvim2 = n.new_session(true, { args = args2, merge = false }) 211 set_session(nvim2) 212 local screen2 = Screen.new(100, 40) 213 screen2:add_extra_attr_ids({ 214 [100] = { foreground = Screen.colors.NvimLightGrey2 }, 215 [101] = { foreground = Screen.colors.NvimLightGreen }, 216 [102] = { 217 foreground = Screen.colors.NvimLightGrey4, 218 background = Screen.colors.NvimDarkGrey1, 219 }, 220 [104] = { foreground = Screen.colors.NvimLightCyan }, 221 [105] = { foreground = Screen.colors.NvimDarkGrey4 }, 222 [106] = { 223 foreground = Screen.colors.NvimLightGrey2, 224 background = Screen.colors.NvimDarkGrey4, 225 }, 226 [107] = { foreground = Screen.colors.NvimLightGrey2, bold = true }, 227 [108] = { foreground = Screen.colors.NvimLightBlue }, 228 }) 229 exec(init) 230 command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog). 231 feed(':edit ' .. testfile .. '<CR>') 232 eq('r?', api.nvim_get_mode().mode) 233 feed('E:source<CR>') 234 eq('r?', api.nvim_get_mode().mode) 235 screen2:sleep(1000) 236 feed('E') 237 screen2:expect([[ 238 {100:^vim.o.foldmethod} {100:=} {101:'expr'} | 239 {100:vim.o.foldexpr} {100:=} {101:'v:lua.vim.treesitter.foldexpr()'} | 240 {102:+-- 3 lines: vim.defer_fn(function()·······························································}| 241 {104:pcall}{100:(vim.cmd.edit,} {101:'Xtest_swapredraw.lua'}{100:)} | 242 {105:~ }|*34 243 {106:Xtest_swapredraw.lua 1,1 All}| 244 | 245 ]]) 246 nvim2:close() 247 end) 248 249 it('always show swapfile dialog #8840 #9027', function() 250 local testfile = 'Xtest_swapdialog_file1' 251 252 local expected_no_dialog = '^' .. (' '):rep(256) .. '|\n' 253 for _ = 1, 37 do 254 expected_no_dialog = expected_no_dialog .. '~' .. (' '):rep(255) .. '|\n' 255 end 256 expected_no_dialog = expected_no_dialog .. testfile .. (' '):rep(216) .. '0,0-1 All|\n' 257 expected_no_dialog = expected_no_dialog .. (' '):rep(256) .. '|\n' 258 259 exec(init) 260 command('edit! ' .. testfile) 261 feed('isometext<esc>') 262 command('preserve') 263 264 -- Start another Nvim instance. 265 local nvim2 = 266 n.new_session(true, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false }) 267 set_session(nvim2) 268 local screen2 = Screen.new(256, 40) 269 screen2._default_attr_ids = nil 270 exec(init) 271 command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog). 272 273 -- With shortmess+=F 274 command('set shortmess+=F') 275 feed(':edit ' .. testfile .. '<CR>') 276 screen2:expect { 277 any = [[E325: ATTENTION.*]] 278 .. '\n' 279 .. [[Found a swap file by the name ".*]] 280 .. [[Xtest_swapdialog_dir[/\].*]] 281 .. testfile 282 .. [[%.swp"]], 283 } 284 feed('e') -- Chose "Edit" at the swap dialog. 285 screen2:expect(expected_no_dialog) 286 287 -- With :silent and shortmess+=F 288 feed(':silent edit %<CR>') 289 screen2:expect { 290 any = [[Found a swap file by the name ".*]] 291 .. [[Xtest_swapdialog_dir[/\].*]] 292 .. testfile 293 .. [[%.swp"]], 294 } 295 feed('e') -- Chose "Edit" at the swap dialog. 296 screen2:expect(expected_no_dialog) 297 298 -- With :silent! and shortmess+=F 299 feed(':silent! edit %<CR>') 300 screen2:expect { 301 any = [[Found a swap file by the name ".*]] 302 .. [[Xtest_swapdialog_dir[/\].*]] 303 .. testfile 304 .. [[%.swp"]], 305 } 306 feed('e') -- Chose "Edit" at the swap dialog. 307 screen2:expect(expected_no_dialog) 308 309 -- With API (via eval/Vimscript) call and shortmess+=F 310 feed(':call nvim_command("edit %")<CR>') 311 screen2:expect { 312 any = [[Found a swap file by the name ".*]] 313 .. [[Xtest_swapdialog_dir[/\].*]] 314 .. testfile 315 .. [[%.swp"]], 316 } 317 feed('e') -- Chose "Edit" at the swap dialog. 318 screen2:expect({ any = pesc('E5555: API call: Vim(edit):E325: ATTENTION') }) 319 feed('<c-c>') 320 screen2:expect(expected_no_dialog) 321 322 -- With API call and shortmess+=F 323 async_meths.nvim_command('edit %') 324 screen2:expect { 325 any = [[Found a swap file by the name ".*]] 326 .. [[Xtest_swapdialog_dir[/\].*]] 327 .. testfile 328 .. [[%.swp"]], 329 } 330 feed('e') -- Chose "Edit" at the swap dialog. 331 expect_msg_seq({ 332 ignore = { 'redraw' }, 333 seqs = { 334 { { 'notification', 'nvim_error_event', { 0, 'Vim(edit):E325: ATTENTION' } } }, 335 }, 336 }) 337 feed('<cr>') 338 339 nvim2:close() 340 end) 341 342 it('default SwapExists handler selects "(E)dit" and skips prompt', function() 343 exec(init) 344 command('edit Xfile1') 345 command("put ='some text...'") 346 command('preserve') -- Make sure the swap file exists. 347 local nvimpid = fn.getpid() 348 349 local nvim1 = n.new_session(true) 350 set_session(nvim1) 351 local screen = Screen.new(75, 18) 352 exec(init) 353 feed(':edit Xfile1\n') 354 355 screen:expect({ any = ('W325: Ignoring swapfile from Nvim process %d'):format(nvimpid) }) 356 nvim1:close() 357 end) 358 359 -- oldtest: Test_swap_prompt_splitwin() 360 it('selecting "q" in the attention prompt', function() 361 exec(init) 362 command('edit Xfile1') 363 command('preserve') -- Make sure the swap file exists. 364 365 local screen = Screen.new(75, 18) 366 local nvim1 = n.new_session(true) 367 set_session(nvim1) 368 screen:attach() 369 exec(init) 370 command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog). 371 feed(':split Xfile1\n') 372 -- The default SwapExists handler does _not_ skip this prompt. 373 screen:expect({ 374 any = pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'), 375 }) 376 feed('q') 377 screen:expect([[ 378 ^ | 379 {1:~ }|*16 380 | 381 ]]) 382 feed(':<CR>') 383 screen:expect([[ 384 ^ | 385 {1:~ }|*16 386 : | 387 ]]) 388 nvim1:close() 389 390 local nvim2 = n.new_session(true) 391 set_session(nvim2) 392 screen:attach() 393 exec(init) 394 command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog). 395 command('set more') 396 command('au bufadd * let foo_w = wincol()') 397 feed(':e Xfile1<CR>') 398 screen:expect({ any = pesc('{6:-- More --}^') }) 399 feed('<Space>') 400 screen:expect({ 401 any = pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'), 402 }) 403 feed('q') 404 command([[echo 'hello']]) 405 screen:expect([[ 406 ^ | 407 {1:~ }|*16 408 hello | 409 ]]) 410 nvim2:close() 411 end) 412 413 --- @param swapexists boolean Enable the default SwapExists handler. 414 --- @param on_swapfile_running fun(screen: any) Called after swapfile ("STILL RUNNING") prompt. 415 local function test_swapfile_after_reboot(swapexists, on_swapfile_running) 416 local screen = Screen.new(75, 30) 417 418 exec(init) 419 if not swapexists then 420 command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog). 421 end 422 command('set nohidden') 423 424 exec([=[ 425 " Make a copy of the current swap file to "Xswap". 426 " Return the name of the swap file. 427 func CopySwapfile() 428 preserve 429 " get the name of the swap file 430 let swname = split(execute("swapname"))[0] 431 let swname = substitute(swname, '[[:blank:][:cntrl:]]*\(.\{-}\)[[:blank:][:cntrl:]]*$', '\1', '') 432 " make a copy of the swap file in Xswap 433 set binary 434 exe 'sp ' . fnameescape(swname) 435 w! Xswap 436 set nobinary 437 return swname 438 endfunc 439 ]=]) 440 441 -- Edit a file and grab its swapfile. 442 exec([[ 443 edit Xswaptest 444 call setline(1, ['a', 'b', 'c']) 445 ]]) 446 local swname = fn.CopySwapfile() 447 448 -- Forget we edited this file 449 exec([[ 450 new 451 only! 452 bwipe! Xswaptest 453 ]]) 454 455 os.rename('Xswap', swname) 456 457 feed(':edit Xswaptest<CR>') 458 on_swapfile_running(screen) 459 460 feed('e') 461 462 -- Forget we edited this file 463 exec([[ 464 new 465 only! 466 bwipe! Xswaptest 467 ]]) 468 469 -- pretend that the swapfile was created before boot 470 local atime = os.time() - uv.uptime() - 10 471 uv.fs_utime(swname, atime, atime) 472 473 feed(':edit Xswaptest<CR>') 474 screen:expect({ 475 any = table.concat({ 476 '{9:E325: ATTENTION}', 477 pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort: }^'), 478 }, '.*'), 479 }) 480 481 feed('e') 482 end 483 484 -- oldtest: Test_nocatch_process_still_running() 485 it('swapfile created before boot vim-patch:8.2.2586', function() 486 test_swapfile_after_reboot(false, function(screen) 487 screen:expect({ 488 any = table.concat({ 489 '{9:E325: ATTENTION}', 490 '{6: process ID: %d* %(STILL RUNNING%)}', 491 '{6:While opening file "Xswaptest"}', 492 pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'), 493 }, '.*'), 494 }) 495 end) 496 end) 497 498 it('swapfile created before boot + default SwapExists handler', function() 499 test_swapfile_after_reboot(true, function(screen) 500 screen:expect({ any = 'W325: Ignoring swapfile from Nvim process' }) 501 end) 502 end) 503 end) 504 505 describe('quitting swapfile dialog on startup stops TUI properly', function() 506 local swapdir = uv.cwd() .. '/Xtest_swapquit_dir' 507 local testfile = 'Xtest_swapquit_file1' 508 local otherfile = 'Xtest_swapquit_file2' 509 -- Put swapdir at the start of the 'directory' list. #1836 510 -- Note: `set swapfile` *must* go after `set directory`: otherwise it may 511 -- attempt to create a swapfile in different directory. 512 local init_dir = [[set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//]] 513 local init_set = [[set swapfile fileformat=unix nomodified undolevels=-1 nohidden]] 514 515 before_each(function() 516 clear({ args = { '--cmd', init_dir, '--cmd', init_set } }) 517 rmdir(swapdir) 518 mkdir(swapdir) 519 write_file( 520 testfile, 521 [[ 522 first 523 second 524 third 525 526 ]] 527 ) 528 command('edit! ' .. testfile) 529 feed('Gisometext<esc>') 530 poke_eventloop() 531 clear() -- Leaves a swap file behind 532 api.nvim_ui_attach(80, 30, {}) 533 end) 534 after_each(function() 535 rmdir(swapdir) 536 os.remove(testfile) 537 os.remove(otherfile) 538 end) 539 540 it('(Q)uit at first file argument', function() 541 local chan = fn.jobstart( 542 { nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', init_dir, '--cmd', init_set, testfile }, 543 { 544 term = true, 545 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 546 } 547 ) 548 retry(nil, nil, function() 549 eq( 550 '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:', 551 eval("getline('$')->trim(' ', 2)") 552 ) 553 end) 554 api.nvim_chan_send(chan, 'q') 555 retry(nil, nil, function() 556 eq( 557 { '', '[Process exited 1]', '' }, 558 eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})") 559 ) 560 end) 561 end) 562 563 it('(A)bort at second file argument with -p', function() 564 local chan = fn.jobstart({ 565 nvim_prog, 566 '-u', 567 'NONE', 568 '-i', 569 'NONE', 570 '--cmd', 571 init_dir, 572 '--cmd', 573 init_set, 574 '-p', 575 otherfile, 576 testfile, 577 }, { 578 term = true, 579 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 580 }) 581 retry(nil, nil, function() 582 eq( 583 '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:', 584 eval("getline('$')->trim(' ', 2)") 585 ) 586 end) 587 api.nvim_chan_send(chan, 'a') 588 retry(nil, nil, function() 589 eq( 590 { '', '[Process exited 1]', '' }, 591 eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})") 592 ) 593 end) 594 end) 595 596 it('(Q)uit at file opened by -t', function() 597 write_file( 598 otherfile, 599 ([[ 600 !_TAG_FILE_ENCODING utf-8 // 601 first %s /^ \zsfirst$/ 602 second %s /^ \zssecond$/ 603 third %s /^ \zsthird$/]]):format(testfile, testfile, testfile) 604 ) 605 local chan = fn.jobstart({ 606 nvim_prog, 607 '-u', 608 'NONE', 609 '-i', 610 'NONE', 611 '--cmd', 612 init_dir, 613 '--cmd', 614 init_set, 615 '--cmd', 616 'set tags=' .. otherfile, 617 '-tsecond', 618 }, { 619 term = true, 620 env = { VIMRUNTIME = os.getenv('VIMRUNTIME') }, 621 }) 622 retry(nil, nil, function() 623 eq( 624 '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:', 625 eval("getline('$')->trim(' ', 2)") 626 ) 627 end) 628 api.nvim_chan_send(chan, 'q') 629 retry(nil, nil, function() 630 eq( 631 { '[Process exited 1]' }, 632 eval( 633 "[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})->filter({_, s -> !empty(trim(s))})" 634 ) 635 ) 636 end) 637 end) 638 end)