fileio_spec.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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 assert_log = t.assert_log
  6. local assert_nolog = t.assert_nolog
  7. local clear = n.clear
  8. local command = n.command
  9. local eq = t.eq
  10. local neq = t.neq
  11. local ok = t.ok
  12. local feed = n.feed
  13. local fn = n.fn
  14. local nvim_prog = n.nvim_prog
  15. local request = n.request
  16. local retry = t.retry
  17. local rmdir = n.rmdir
  18. local matches = t.matches
  19. local api = n.api
  20. local mkdir = t.mkdir
  21. local sleep = vim.uv.sleep
  22. local read_file = t.read_file
  23. local trim = vim.trim
  24. local currentdir = n.fn.getcwd
  25. local assert_alive = n.assert_alive
  26. local check_close = n.check_close
  27. local expect_exit = n.expect_exit
  28. local write_file = t.write_file
  29. local feed_command = n.feed_command
  30. local skip = t.skip
  31. local is_os = t.is_os
  32. local is_ci = t.is_ci
  33. local spawn = n.spawn
  34. local set_session = n.set_session
  35. describe('fileio', function()
  36. before_each(function() end)
  37. after_each(function()
  38. check_close()
  39. os.remove('Xtest_startup_shada')
  40. os.remove('Xtest_startup_file1')
  41. os.remove('Xtest_startup_file1~')
  42. os.remove('Xtest_startup_file2')
  43. os.remove('Xtest_startup_file2~')
  44. os.remove('Xtest_тест.md')
  45. os.remove('Xtest-u8-int-max')
  46. os.remove('Xtest-overwrite-forced')
  47. rmdir('Xtest_startup_swapdir')
  48. rmdir('Xtest_backupdir')
  49. rmdir('Xtest_backupdir with spaces')
  50. end)
  51. local args = { nvim_prog, '--clean', '--cmd', 'set nofsync directory=Xtest_startup_swapdir' }
  52. --- Starts a new nvim session and returns an attached screen.
  53. local function startup(extra_args)
  54. extra_args = extra_args or {}
  55. local argv = vim.iter({ args, '--embed', extra_args }):flatten():totable()
  56. local screen_nvim = spawn(argv)
  57. set_session(screen_nvim)
  58. local screen = Screen.new(70, 10)
  59. screen:set_default_attr_ids({
  60. [1] = { foreground = Screen.colors.NvimDarkGrey4 },
  61. [2] = { background = Screen.colors.NvimDarkGrey1, foreground = Screen.colors.NvimLightGrey3 },
  62. [3] = { foreground = Screen.colors.NvimLightCyan },
  63. })
  64. return screen
  65. end
  66. it("fsync() with 'nofsync' #8304", function()
  67. clear({ args = { '--cmd', 'set nofsync directory=Xtest_startup_swapdir' } })
  68. -- These cases ALWAYS force fsync (regardless of 'fsync' option):
  69. -- 1. Idle (CursorHold) with modified buffers (+ 'swapfile').
  70. command('write Xtest_startup_file1')
  71. feed('Afoo<esc>h')
  72. command('write')
  73. eq(0, request('nvim__stats').fsync)
  74. command('set swapfile')
  75. command('set updatetime=1')
  76. feed('Azub<esc>h') -- File is 'modified'.
  77. sleep(3) -- Allow 'updatetime' to expire.
  78. retry(3, nil, function()
  79. eq(1, request('nvim__stats').fsync)
  80. end)
  81. command('set updatetime=100000 updatecount=100000')
  82. -- 2. Explicit :preserve command.
  83. command('preserve')
  84. -- TODO: should be exactly 2; where is the extra fsync() is coming from? #26404
  85. ok(request('nvim__stats').fsync == 2 or request('nvim__stats').fsync == 3)
  86. -- 3. Enable 'fsync' option, write file.
  87. command('set fsync')
  88. feed('Abaz<esc>h')
  89. command('write')
  90. -- TODO: should be exactly 4; where is the extra fsync() is coming from? #26404
  91. ok(request('nvim__stats').fsync == 4 or request('nvim__stats').fsync == 5)
  92. eq('foozubbaz', trim(read_file('Xtest_startup_file1')))
  93. -- 4. Exit caused by deadly signal (+ 'swapfile').
  94. local j = fn.jobstart(vim.iter({ args, '--embed' }):flatten():totable(), { rpc = true })
  95. fn.rpcrequest(
  96. j,
  97. 'nvim_exec2',
  98. [[
  99. set nofsync directory=Xtest_startup_swapdir
  100. edit Xtest_startup_file2
  101. write
  102. put ='fsyncd text'
  103. ]],
  104. {}
  105. )
  106. eq('Xtest_startup_swapdir', fn.rpcrequest(j, 'nvim_eval', '&directory'))
  107. fn.jobstop(j) -- Send deadly signal.
  108. local screen = startup()
  109. feed(':recover Xtest_startup_file2<cr>')
  110. screen:expect({ any = [[Using swap file "Xtest_startup_swapdir[/\]Xtest_startup_file2%.swp"]] })
  111. feed('<cr>')
  112. screen:expect({ any = 'fsyncd text' })
  113. -- 5. SIGPWR signal.
  114. -- oldtest: Test_signal_PWR()
  115. end)
  116. it('backup #9709', function()
  117. skip(is_ci('cirrus'))
  118. clear({
  119. args = {
  120. '-i',
  121. 'Xtest_startup_shada',
  122. '--cmd',
  123. 'set directory=Xtest_startup_swapdir',
  124. },
  125. })
  126. command('write Xtest_startup_file1')
  127. feed('ifoo<esc>')
  128. command('set backup')
  129. command('set backupcopy=yes')
  130. command('write')
  131. feed('Abar<esc>')
  132. command('write')
  133. local foobar_contents = trim(read_file('Xtest_startup_file1'))
  134. local bar_contents = trim(read_file('Xtest_startup_file1~'))
  135. eq('foobar', foobar_contents)
  136. eq('foo', bar_contents)
  137. end)
  138. it('backup with full path #11214', function()
  139. skip(is_ci('cirrus'))
  140. clear()
  141. mkdir('Xtest_backupdir')
  142. command('set backup')
  143. command('set backupdir=Xtest_backupdir//')
  144. command('write Xtest_startup_file1')
  145. feed('ifoo<esc>')
  146. command('write')
  147. feed('Abar<esc>')
  148. command('write')
  149. -- Backup filename = fullpath, separators replaced with "%".
  150. local backup_file_name = string.gsub(
  151. currentdir() .. '/Xtest_startup_file1',
  152. is_os('win') and '[:/\\]' or '/',
  153. '%%'
  154. ) .. '~'
  155. local foo_contents = trim(read_file('Xtest_backupdir/' .. backup_file_name))
  156. local foobar_contents = trim(read_file('Xtest_startup_file1'))
  157. eq('foobar', foobar_contents)
  158. eq('foo', foo_contents)
  159. end)
  160. it('backup with full path with spaces', function()
  161. skip(is_ci('cirrus'))
  162. clear()
  163. mkdir('Xtest_backupdir with spaces')
  164. command('set backup')
  165. command('set backupdir=Xtest_backupdir\\ with\\ spaces//')
  166. command('write Xtest_startup_file1')
  167. feed('ifoo<esc>')
  168. command('write')
  169. feed('Abar<esc>')
  170. command('write')
  171. -- Backup filename = fullpath, separators replaced with "%".
  172. local backup_file_name = string.gsub(
  173. currentdir() .. '/Xtest_startup_file1',
  174. is_os('win') and '[:/\\]' or '/',
  175. '%%'
  176. ) .. '~'
  177. local foo_contents = trim(read_file('Xtest_backupdir with spaces/' .. backup_file_name))
  178. local foobar_contents = trim(read_file('Xtest_startup_file1'))
  179. eq('foobar', foobar_contents)
  180. eq('foo', foo_contents)
  181. end)
  182. it('backup symlinked files #11349', function()
  183. skip(is_ci('cirrus'))
  184. clear()
  185. local initial_content = 'foo'
  186. local link_file_name = 'Xtest_startup_file2'
  187. local backup_file_name = link_file_name .. '~'
  188. write_file('Xtest_startup_file1', initial_content, false)
  189. uv.fs_symlink('Xtest_startup_file1', link_file_name)
  190. command('set backup')
  191. command('set backupcopy=yes')
  192. command('edit ' .. link_file_name)
  193. feed('Abar<esc>')
  194. command('write')
  195. local backup_raw = read_file(backup_file_name)
  196. neq(nil, backup_raw, 'Expected backup file ' .. backup_file_name .. 'to exist but did not')
  197. eq(initial_content, trim(backup_raw), 'Expected backup to contain original contents')
  198. end)
  199. it('backup symlinked files in first available backupdir #11349', function()
  200. skip(is_ci('cirrus'))
  201. clear()
  202. local initial_content = 'foo'
  203. local backup_dir = 'Xtest_backupdir'
  204. local sep = n.get_pathsep()
  205. local link_file_name = 'Xtest_startup_file2'
  206. local backup_file_name = backup_dir .. sep .. link_file_name .. '~'
  207. write_file('Xtest_startup_file1', initial_content, false)
  208. uv.fs_symlink('Xtest_startup_file1', link_file_name)
  209. mkdir(backup_dir)
  210. command('set backup')
  211. command('set backupcopy=yes')
  212. command('set backupdir=.__this_does_not_exist__,' .. backup_dir)
  213. command('edit ' .. link_file_name)
  214. feed('Abar<esc>')
  215. command('write')
  216. local backup_raw = read_file(backup_file_name)
  217. neq(nil, backup_raw, 'Expected backup file ' .. backup_file_name .. ' to exist but did not')
  218. eq(initial_content, trim(backup_raw), 'Expected backup to contain original contents')
  219. end)
  220. it('readfile() on multibyte filename #10586', function()
  221. clear()
  222. local text = {
  223. 'line1',
  224. ' ...line2... ',
  225. '',
  226. 'line3!',
  227. 'тест yay тест.',
  228. '',
  229. }
  230. local fname = 'Xtest_тест.md'
  231. fn.writefile(text, fname, 's')
  232. table.insert(text, '')
  233. eq(text, fn.readfile(fname, 'b'))
  234. end)
  235. it("read invalid u8 over INT_MAX doesn't segfault", function()
  236. clear()
  237. command('call writefile(0zFFFFFFFF, "Xtest-u8-int-max")')
  238. -- This should not segfault
  239. command('edit ++enc=utf32 Xtest-u8-int-max')
  240. assert_alive()
  241. end)
  242. it(':w! does not show "file has been changed" warning', function()
  243. clear()
  244. write_file('Xtest-overwrite-forced', 'foobar')
  245. command('set nofixendofline')
  246. local screen = Screen.new(40, 4)
  247. command('set shortmess-=F')
  248. command('e Xtest-overwrite-forced')
  249. screen:expect([[
  250. ^foobar |
  251. {1:~ }|*2
  252. "Xtest-overwrite-forced" [noeol] 1L, 6B |
  253. ]])
  254. -- Get current unix time.
  255. local cur_unix_time = os.time(os.date('!*t'))
  256. local future_time = cur_unix_time + 999999
  257. -- Set the file's access/update time to be
  258. -- greater than the time at which it was created.
  259. uv.fs_utime('Xtest-overwrite-forced', future_time, future_time)
  260. -- use async feed_command because nvim basically hangs on the prompt
  261. feed_command('w')
  262. screen:expect([[
  263. {9:WARNING: The file has been changed since}|
  264. {9: reading it!!!} |
  265. {6:Do you really want to write to it (y/n)?}|
  266. ^ |
  267. ]])
  268. feed('n')
  269. feed('<cr>')
  270. screen:expect([[
  271. ^foobar |
  272. {1:~ }|*2
  273. |
  274. ]])
  275. -- Use a screen test because the warning does not set v:errmsg.
  276. command('w!')
  277. screen:expect([[
  278. ^foobar |
  279. {1:~ }|*2
  280. <erwrite-forced" [noeol] 1L, 6B written |
  281. ]])
  282. end)
  283. end)
  284. describe('tmpdir', function()
  285. local tmproot_pat = [=[.*[/\\]nvim%.[^/\\]+]=]
  286. local testlog = 'Xtest_tmpdir_log'
  287. local os_tmpdir ---@type string
  288. before_each(function()
  289. -- Fake /tmp dir so that we can mess it up.
  290. os_tmpdir = assert(vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX'))
  291. end)
  292. after_each(function()
  293. check_close()
  294. os.remove(testlog)
  295. end)
  296. local function get_tmproot()
  297. -- Tempfiles typically look like: "…/nvim.<user>/xxx/0".
  298. -- - "…/nvim.<user>/xxx/" is the per-process tmpdir, not shared with other Nvims.
  299. -- - "…/nvim.<user>/" is the tmpdir root, shared by all Nvims (normally).
  300. local tmproot = (fn.tempname()):match(tmproot_pat)
  301. ok(tmproot:len() > 4, 'tmproot like "nvim.foo"', tmproot)
  302. return tmproot
  303. end
  304. it('failure modes', function()
  305. clear({ env = { NVIM_LOG_FILE = testlog, TMPDIR = os_tmpdir } })
  306. assert_nolog('tempdir is not a directory', testlog)
  307. assert_nolog('tempdir has invalid permissions', testlog)
  308. local tmproot = get_tmproot()
  309. -- Test how Nvim handles invalid tmpdir root (by hostile users or accidents).
  310. --
  311. -- "…/nvim.<user>/" is not a directory:
  312. expect_exit(command, ':qall!')
  313. rmdir(tmproot)
  314. write_file(tmproot, '') -- Not a directory, vim_mktempdir() should skip it.
  315. clear({ env = { NVIM_LOG_FILE = testlog, TMPDIR = os_tmpdir } })
  316. matches(tmproot_pat, fn.stdpath('run')) -- Tickle vim_mktempdir().
  317. -- Assert that broken tmpdir root was handled.
  318. assert_log('tempdir root not a directory', testlog, 100)
  319. -- "…/nvim.<user>/" has wrong permissions:
  320. skip(is_os('win'), 'TODO(justinmk): need setfperm/getfperm on Windows. #8244')
  321. os.remove(testlog)
  322. os.remove(tmproot)
  323. mkdir(tmproot)
  324. fn.setfperm(tmproot, 'rwxr--r--') -- Invalid permissions, vim_mktempdir() should skip it.
  325. clear({ env = { NVIM_LOG_FILE = testlog, TMPDIR = os_tmpdir } })
  326. matches(tmproot_pat, fn.stdpath('run')) -- Tickle vim_mktempdir().
  327. -- Assert that broken tmpdir root was handled.
  328. assert_log('tempdir root has invalid permissions', testlog, 100)
  329. end)
  330. it('too long', function()
  331. local bigname = ('%s/%s'):format(os_tmpdir, ('x'):rep(666))
  332. mkdir(bigname)
  333. clear({ env = { NVIM_LOG_FILE = testlog, TMPDIR = bigname } })
  334. matches(tmproot_pat, fn.stdpath('run')) -- Tickle vim_mktempdir().
  335. local len = (fn.tempname()):len()
  336. ok(len > 4 and len < 256, '4 < len < 256', tostring(len))
  337. end)
  338. it('disappeared #1432', function()
  339. clear({ env = { NVIM_LOG_FILE = testlog, TMPDIR = os_tmpdir } })
  340. assert_nolog('tempdir disappeared', testlog)
  341. local function rm_tmpdir()
  342. local tmpname1 = fn.tempname()
  343. local tmpdir1 = fn.fnamemodify(tmpname1, ':h')
  344. eq(fn.stdpath('run'), tmpdir1)
  345. rmdir(tmpdir1)
  346. retry(nil, 1000, function()
  347. eq(0, fn.isdirectory(tmpdir1))
  348. end)
  349. local tmpname2 = fn.tempname()
  350. local tmpdir2 = fn.fnamemodify(tmpname2, ':h')
  351. neq(tmpdir1, tmpdir2)
  352. end
  353. -- Your antivirus hates you...
  354. rm_tmpdir()
  355. assert_log('tempdir disappeared', testlog, 100)
  356. fn.tempname()
  357. fn.tempname()
  358. fn.tempname()
  359. eq('', api.nvim_get_vvar('errmsg'))
  360. rm_tmpdir()
  361. fn.tempname()
  362. fn.tempname()
  363. fn.tempname()
  364. eq('E5431: tempdir disappeared (2 times)', api.nvim_get_vvar('errmsg'))
  365. rm_tmpdir()
  366. eq('E5431: tempdir disappeared (3 times)', api.nvim_get_vvar('errmsg'))
  367. end)
  368. end)