fileio_spec.lua 13 KB

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