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