neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)