swapfile_preserve_recover_spec.lua 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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. local eq, eval, expect, exec = t.eq, n.eval, n.expect, n.exec
  6. local assert_alive = n.assert_alive
  7. local clear = n.clear
  8. local command = n.command
  9. local feed = n.feed
  10. local fn = n.fn
  11. local nvim_prog = n.nvim_prog
  12. local ok = t.ok
  13. local rmdir = n.rmdir
  14. local new_pipename = n.new_pipename
  15. local pesc = vim.pesc
  16. local os_kill = n.os_kill
  17. local set_session = n.set_session
  18. local async_meths = n.async_meths
  19. local expect_msg_seq = n.expect_msg_seq
  20. local pcall_err = t.pcall_err
  21. local mkdir = t.mkdir
  22. local poke_eventloop = n.poke_eventloop
  23. local api = n.api
  24. local retry = t.retry
  25. local write_file = t.write_file
  26. describe(':recover', function()
  27. before_each(clear)
  28. it('fails if given a non-existent swapfile', function()
  29. local swapname = 'bogus_swapfile'
  30. local swapname2 = 'bogus_swapfile.swp'
  31. eq(
  32. 'Vim(recover):E305: No swap file found for ' .. swapname,
  33. pcall_err(command, 'recover ' .. swapname)
  34. ) -- Should not segfault. #2117
  35. -- Also check filename ending with ".swp". #9504
  36. eq('Vim(recover):E306: Cannot open ' .. swapname2, pcall_err(command, 'recover ' .. swapname2)) -- Should not segfault. #2117
  37. assert_alive()
  38. end)
  39. end)
  40. describe("preserve and (R)ecover with custom 'directory'", function()
  41. local swapdir = uv.cwd() .. '/Xtest_recover_dir'
  42. local testfile = 'Xtest_recover_file1'
  43. -- Put swapdir at the start of the 'directory' list. #1836
  44. -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
  45. -- attempt to create a swapfile in different directory.
  46. local init = [[
  47. set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//
  48. set swapfile fileformat=unix undolevels=-1
  49. ]]
  50. local nvim0
  51. before_each(function()
  52. nvim0 = n.new_session(false)
  53. set_session(nvim0)
  54. rmdir(swapdir)
  55. mkdir(swapdir)
  56. end)
  57. after_each(function()
  58. command('%bwipeout!')
  59. rmdir(swapdir)
  60. end)
  61. local function setup_swapname()
  62. exec(init)
  63. command('edit! ' .. testfile)
  64. feed('isometext<esc>')
  65. exec('redir => g:swapname | silent swapname | redir END')
  66. return eval('g:swapname')
  67. end
  68. local function test_recover(swappath1)
  69. -- Start another Nvim instance.
  70. local nvim2 =
  71. n.new_session(false, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false })
  72. set_session(nvim2)
  73. exec(init)
  74. -- Use the "SwapExists" event to choose the (R)ecover choice at the dialog.
  75. command('autocmd SwapExists * let v:swapchoice = "r"')
  76. command('silent edit! ' .. testfile)
  77. exec('redir => g:swapname | silent swapname | redir END')
  78. local swappath2 = eval('g:swapname')
  79. expect('sometext')
  80. -- swapfile from session 1 should end in .swp
  81. eq(testfile .. '.swp', string.match(swappath1, '[^%%]+$'))
  82. -- swapfile from session 2 should end in .swo
  83. eq(testfile .. '.swo', string.match(swappath2, '[^%%]+$'))
  84. -- Verify that :swapname was not truncated (:help 'shortmess').
  85. ok(nil == string.find(swappath1, '%.%.%.'))
  86. ok(nil == string.find(swappath2, '%.%.%.'))
  87. end
  88. it('with :preserve and SIGKILL', function()
  89. local swappath1 = setup_swapname()
  90. command('preserve')
  91. os_kill(eval('getpid()'))
  92. test_recover(swappath1)
  93. end)
  94. it('closing stdio channel without :preserve #22096', function()
  95. local swappath1 = setup_swapname()
  96. nvim0:close()
  97. test_recover(swappath1)
  98. end)
  99. it('killing TUI process without :preserve #22096', function()
  100. t.skip(t.is_os('win'))
  101. local screen0 = Screen.new()
  102. local child_server = new_pipename()
  103. fn.jobstart({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--listen', child_server }, {
  104. term = true,
  105. env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
  106. })
  107. screen0:expect({ any = pesc('[No Name]') }) -- Wait for the child process to start.
  108. local child_session = n.connect(child_server)
  109. set_session(child_session)
  110. local swappath1 = setup_swapname()
  111. set_session(nvim0)
  112. command('call chanclose(&channel)') -- Kill the child process.
  113. screen0:expect({ any = pesc('[Process exited 1]') }) -- Wait for the child process to stop.
  114. test_recover(swappath1)
  115. end)
  116. end)
  117. describe('swapfile detection', function()
  118. local swapdir = uv.cwd() .. '/Xtest_swapdialog_dir'
  119. local nvim0
  120. -- Put swapdir at the start of the 'directory' list. #1836
  121. -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
  122. -- attempt to create a swapfile in different directory.
  123. local init = [[
  124. set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//
  125. set swapfile fileformat=unix nomodified undolevels=-1 nohidden
  126. ]]
  127. before_each(function()
  128. nvim0 = n.new_session(false)
  129. set_session(nvim0)
  130. rmdir(swapdir)
  131. mkdir(swapdir)
  132. end)
  133. after_each(function()
  134. set_session(nvim0)
  135. command('%bwipeout!')
  136. rmdir(swapdir)
  137. end)
  138. it('always show swapfile dialog #8840 #9027', function()
  139. local testfile = 'Xtest_swapdialog_file1'
  140. local expected_no_dialog = '^' .. (' '):rep(256) .. '|\n'
  141. for _ = 1, 37 do
  142. expected_no_dialog = expected_no_dialog .. '~' .. (' '):rep(255) .. '|\n'
  143. end
  144. expected_no_dialog = expected_no_dialog .. testfile .. (' '):rep(216) .. '0,0-1 All|\n'
  145. expected_no_dialog = expected_no_dialog .. (' '):rep(256) .. '|\n'
  146. exec(init)
  147. command('edit! ' .. testfile)
  148. feed('isometext<esc>')
  149. command('preserve')
  150. -- Start another Nvim instance.
  151. local nvim2 =
  152. n.new_session(true, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false })
  153. set_session(nvim2)
  154. local screen2 = Screen.new(256, 40)
  155. screen2._default_attr_ids = nil
  156. exec(init)
  157. command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
  158. -- With shortmess+=F
  159. command('set shortmess+=F')
  160. feed(':edit ' .. testfile .. '<CR>')
  161. screen2:expect {
  162. any = [[E325: ATTENTION.*]]
  163. .. '\n'
  164. .. [[Found a swap file by the name ".*]]
  165. .. [[Xtest_swapdialog_dir[/\].*]]
  166. .. testfile
  167. .. [[%.swp"]],
  168. }
  169. feed('e') -- Chose "Edit" at the swap dialog.
  170. screen2:expect(expected_no_dialog)
  171. -- With :silent and shortmess+=F
  172. feed(':silent edit %<CR>')
  173. screen2:expect {
  174. any = [[Found a swap file by the name ".*]]
  175. .. [[Xtest_swapdialog_dir[/\].*]]
  176. .. testfile
  177. .. [[%.swp"]],
  178. }
  179. feed('e') -- Chose "Edit" at the swap dialog.
  180. screen2:expect(expected_no_dialog)
  181. -- With :silent! and shortmess+=F
  182. feed(':silent! edit %<CR>')
  183. screen2:expect {
  184. any = [[Found a swap file by the name ".*]]
  185. .. [[Xtest_swapdialog_dir[/\].*]]
  186. .. testfile
  187. .. [[%.swp"]],
  188. }
  189. feed('e') -- Chose "Edit" at the swap dialog.
  190. screen2:expect(expected_no_dialog)
  191. -- With API (via eval/Vimscript) call and shortmess+=F
  192. feed(':call nvim_command("edit %")<CR>')
  193. screen2:expect {
  194. any = [[Found a swap file by the name ".*]]
  195. .. [[Xtest_swapdialog_dir[/\].*]]
  196. .. testfile
  197. .. [[%.swp"]],
  198. }
  199. feed('e') -- Chose "Edit" at the swap dialog.
  200. screen2:expect({ any = pesc('E5555: API call: Vim(edit):E325: ATTENTION') })
  201. feed('<c-c>')
  202. screen2:expect(expected_no_dialog)
  203. -- With API call and shortmess+=F
  204. async_meths.nvim_command('edit %')
  205. screen2:expect {
  206. any = [[Found a swap file by the name ".*]]
  207. .. [[Xtest_swapdialog_dir[/\].*]]
  208. .. testfile
  209. .. [[%.swp"]],
  210. }
  211. feed('e') -- Chose "Edit" at the swap dialog.
  212. expect_msg_seq({
  213. ignore = { 'redraw' },
  214. seqs = {
  215. { { 'notification', 'nvim_error_event', { 0, 'Vim(edit):E325: ATTENTION' } } },
  216. },
  217. })
  218. feed('<cr>')
  219. nvim2:close()
  220. end)
  221. it('default SwapExists handler selects "(E)dit" and skips prompt', function()
  222. exec(init)
  223. command('edit Xfile1')
  224. command("put ='some text...'")
  225. command('preserve') -- Make sure the swap file exists.
  226. local nvimpid = fn.getpid()
  227. local nvim1 = n.new_session(true)
  228. set_session(nvim1)
  229. local screen = Screen.new(75, 18)
  230. exec(init)
  231. feed(':edit Xfile1\n')
  232. screen:expect({ any = ('W325: Ignoring swapfile from Nvim process %d'):format(nvimpid) })
  233. nvim1:close()
  234. end)
  235. -- oldtest: Test_swap_prompt_splitwin()
  236. it('selecting "q" in the attention prompt', function()
  237. exec(init)
  238. command('edit Xfile1')
  239. command('preserve') -- Make sure the swap file exists.
  240. local screen = Screen.new(75, 18)
  241. screen:set_default_attr_ids({
  242. [0] = { bold = true, foreground = Screen.colors.Blue }, -- NonText
  243. [1] = { bold = true, foreground = Screen.colors.SeaGreen }, -- MoreMsg
  244. })
  245. local nvim1 = n.new_session(true)
  246. set_session(nvim1)
  247. screen:attach()
  248. exec(init)
  249. command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
  250. feed(':split Xfile1\n')
  251. -- The default SwapExists handler does _not_ skip this prompt.
  252. screen:expect({
  253. any = pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
  254. })
  255. feed('q')
  256. feed(':<CR>')
  257. screen:expect([[
  258. ^ |
  259. {0:~ }|*16
  260. : |
  261. ]])
  262. nvim1:close()
  263. local nvim2 = n.new_session(true)
  264. set_session(nvim2)
  265. screen:attach()
  266. exec(init)
  267. command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
  268. command('set more')
  269. command('au bufadd * let foo_w = wincol()')
  270. feed(':e Xfile1<CR>')
  271. screen:expect({ any = pesc('{1:-- More --}^') })
  272. feed('<Space>')
  273. screen:expect({
  274. any = pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
  275. })
  276. feed('q')
  277. command([[echo 'hello']])
  278. screen:expect([[
  279. ^ |
  280. {0:~ }|*16
  281. hello |
  282. ]])
  283. nvim2:close()
  284. end)
  285. --- @param swapexists boolean Enable the default SwapExists handler.
  286. --- @param on_swapfile_running fun(screen: any) Called after swapfile ("STILL RUNNING") prompt.
  287. local function test_swapfile_after_reboot(swapexists, on_swapfile_running)
  288. local screen = Screen.new(75, 30)
  289. screen:set_default_attr_ids({
  290. [0] = { bold = true, foreground = Screen.colors.Blue }, -- NonText
  291. [1] = { bold = true, foreground = Screen.colors.SeaGreen }, -- MoreMsg
  292. [2] = { background = Screen.colors.Red, foreground = Screen.colors.White }, -- ErrorMsg
  293. })
  294. exec(init)
  295. if not swapexists then
  296. command('autocmd! nvim_swapfile') -- Delete the default handler (which skips the dialog).
  297. end
  298. command('set nohidden')
  299. exec([=[
  300. " Make a copy of the current swap file to "Xswap".
  301. " Return the name of the swap file.
  302. func CopySwapfile()
  303. preserve
  304. " get the name of the swap file
  305. let swname = split(execute("swapname"))[0]
  306. let swname = substitute(swname, '[[:blank:][:cntrl:]]*\(.\{-}\)[[:blank:][:cntrl:]]*$', '\1', '')
  307. " make a copy of the swap file in Xswap
  308. set binary
  309. exe 'sp ' . fnameescape(swname)
  310. w! Xswap
  311. set nobinary
  312. return swname
  313. endfunc
  314. ]=])
  315. -- Edit a file and grab its swapfile.
  316. exec([[
  317. edit Xswaptest
  318. call setline(1, ['a', 'b', 'c'])
  319. ]])
  320. local swname = fn.CopySwapfile()
  321. -- Forget we edited this file
  322. exec([[
  323. new
  324. only!
  325. bwipe! Xswaptest
  326. ]])
  327. os.rename('Xswap', swname)
  328. feed(':edit Xswaptest<CR>')
  329. on_swapfile_running(screen)
  330. feed('e')
  331. -- Forget we edited this file
  332. exec([[
  333. new
  334. only!
  335. bwipe! Xswaptest
  336. ]])
  337. -- pretend that the swapfile was created before boot
  338. local atime = os.time() - uv.uptime() - 10
  339. uv.fs_utime(swname, atime, atime)
  340. feed(':edit Xswaptest<CR>')
  341. screen:expect({
  342. any = table.concat({
  343. pesc('{2:E325: ATTENTION}'),
  344. pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort: }^'),
  345. }, '.*'),
  346. })
  347. feed('e')
  348. end
  349. -- oldtest: Test_nocatch_process_still_running()
  350. it('swapfile created before boot vim-patch:8.2.2586', function()
  351. test_swapfile_after_reboot(false, function(screen)
  352. screen:expect({
  353. any = table.concat({
  354. pesc('{2:E325: ATTENTION}'),
  355. 'file name: .*Xswaptest',
  356. 'process ID: %d* %(STILL RUNNING%)',
  357. pesc('{1:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
  358. }, '.*'),
  359. })
  360. end)
  361. end)
  362. it('swapfile created before boot + default SwapExists handler', function()
  363. test_swapfile_after_reboot(true, function(screen)
  364. screen:expect({ any = 'W325: Ignoring swapfile from Nvim process' })
  365. end)
  366. end)
  367. end)
  368. describe('quitting swapfile dialog on startup stops TUI properly', function()
  369. local swapdir = uv.cwd() .. '/Xtest_swapquit_dir'
  370. local testfile = 'Xtest_swapquit_file1'
  371. local otherfile = 'Xtest_swapquit_file2'
  372. -- Put swapdir at the start of the 'directory' list. #1836
  373. -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
  374. -- attempt to create a swapfile in different directory.
  375. local init_dir = [[set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//]]
  376. local init_set = [[set swapfile fileformat=unix nomodified undolevels=-1 nohidden]]
  377. before_each(function()
  378. clear({ args = { '--cmd', init_dir, '--cmd', init_set } })
  379. rmdir(swapdir)
  380. mkdir(swapdir)
  381. write_file(
  382. testfile,
  383. [[
  384. first
  385. second
  386. third
  387. ]]
  388. )
  389. command('edit! ' .. testfile)
  390. feed('Gisometext<esc>')
  391. poke_eventloop()
  392. clear() -- Leaves a swap file behind
  393. api.nvim_ui_attach(80, 30, {})
  394. end)
  395. after_each(function()
  396. rmdir(swapdir)
  397. os.remove(testfile)
  398. os.remove(otherfile)
  399. end)
  400. it('(Q)uit at first file argument', function()
  401. local chan = fn.jobstart(
  402. { nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', init_dir, '--cmd', init_set, testfile },
  403. {
  404. term = true,
  405. env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
  406. }
  407. )
  408. retry(nil, nil, function()
  409. eq(
  410. '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
  411. eval("getline('$')->trim(' ', 2)")
  412. )
  413. end)
  414. api.nvim_chan_send(chan, 'q')
  415. retry(nil, nil, function()
  416. eq(
  417. { '', '[Process exited 1]', '' },
  418. eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})")
  419. )
  420. end)
  421. end)
  422. it('(A)bort at second file argument with -p', function()
  423. local chan = fn.jobstart({
  424. nvim_prog,
  425. '-u',
  426. 'NONE',
  427. '-i',
  428. 'NONE',
  429. '--cmd',
  430. init_dir,
  431. '--cmd',
  432. init_set,
  433. '-p',
  434. otherfile,
  435. testfile,
  436. }, {
  437. term = true,
  438. env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
  439. })
  440. retry(nil, nil, function()
  441. eq(
  442. '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
  443. eval("getline('$')->trim(' ', 2)")
  444. )
  445. end)
  446. api.nvim_chan_send(chan, 'a')
  447. retry(nil, nil, function()
  448. eq(
  449. { '', '[Process exited 1]', '' },
  450. eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})")
  451. )
  452. end)
  453. end)
  454. it('(Q)uit at file opened by -t', function()
  455. write_file(
  456. otherfile,
  457. ([[
  458. !_TAG_FILE_ENCODING utf-8 //
  459. first %s /^ \zsfirst$/
  460. second %s /^ \zssecond$/
  461. third %s /^ \zsthird$/]]):format(testfile, testfile, testfile)
  462. )
  463. local chan = fn.jobstart({
  464. nvim_prog,
  465. '-u',
  466. 'NONE',
  467. '-i',
  468. 'NONE',
  469. '--cmd',
  470. init_dir,
  471. '--cmd',
  472. init_set,
  473. '--cmd',
  474. 'set tags=' .. otherfile,
  475. '-tsecond',
  476. }, {
  477. term = true,
  478. env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
  479. })
  480. retry(nil, nil, function()
  481. eq(
  482. '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
  483. eval("getline('$')->trim(' ', 2)")
  484. )
  485. end)
  486. api.nvim_chan_send(chan, 'q')
  487. retry(nil, nil, function()
  488. eq(
  489. { '', '[Process exited 1]', '' },
  490. eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})")
  491. )
  492. end)
  493. end)
  494. end)