neovim

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

fs_spec.lua (25345B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 
      4 local clear = n.clear
      5 local exec_lua = n.exec_lua
      6 local eq = t.eq
      7 local mkdir_p = n.mkdir_p
      8 local rmdir = n.rmdir
      9 local nvim_dir = n.nvim_dir
     10 local command = n.command
     11 local api = n.api
     12 local fn = n.fn
     13 local test_build_dir = t.paths.test_build_dir
     14 local test_source_path = t.paths.test_source_path
     15 local nvim_prog = n.nvim_prog
     16 local is_os = t.is_os
     17 local mkdir = t.mkdir
     18 
     19 local nvim_prog_basename = is_os('win') and 'nvim.exe' or 'nvim'
     20 
     21 local link_limit = is_os('win') and 64 or (is_os('mac') or is_os('bsd')) and 33 or 41
     22 
     23 local test_basename_dirname_eq = {
     24  '~/foo/',
     25  '~/foo',
     26  '~/foo/bar.lua',
     27  'foo.lua',
     28  ' ',
     29  '',
     30  '.',
     31  '..',
     32  '../',
     33  '~',
     34  '/usr/bin',
     35  '/usr/bin/gcc',
     36  '/',
     37  '/usr/',
     38  '/usr',
     39  'c:/usr',
     40  'c:/',
     41  'c:',
     42  'c:/users/foo',
     43  'c:/users/foo/bar.lua',
     44  'c:/users/foo/bar/../',
     45  '~/foo/bar\\baz',
     46 }
     47 
     48 local tests_windows_paths = {
     49  'c:\\usr',
     50  'c:\\',
     51  'c:',
     52  'c:\\users\\foo',
     53  'c:\\users\\foo\\bar.lua',
     54  'c:\\users\\foo\\bar\\..\\',
     55 }
     56 
     57 setup(clear)
     58 
     59 describe('vim.fs', function()
     60  describe('parents()', function()
     61    it('works', function()
     62      local test_dir = nvim_dir .. '/test'
     63      mkdir_p(test_dir)
     64      local dirs = {} --- @type string[]
     65      for dir in vim.fs.parents(test_dir .. '/foo.txt') do
     66        dirs[#dirs + 1] = dir
     67        if dir == test_build_dir then
     68          break
     69        end
     70      end
     71      eq({ test_dir, nvim_dir, test_build_dir }, dirs)
     72      rmdir(test_dir)
     73    end)
     74  end)
     75 
     76  describe('dirname()', function()
     77    it('works', function()
     78      eq(test_build_dir, vim.fs.dirname(nvim_dir))
     79 
     80      ---@param paths string[]
     81      ---@param is_win? boolean
     82      local function test_paths(paths, is_win)
     83        local gsub = is_win and [[:gsub('\\', '/')]] or ''
     84        local code = string.format(
     85          [[
     86          local path = ...
     87          return vim.fn.fnamemodify(path,':h')%s
     88        ]],
     89          gsub
     90        )
     91 
     92        for _, path in ipairs(paths) do
     93          eq(exec_lua(code, path), vim.fs.dirname(path), path)
     94        end
     95      end
     96 
     97      test_paths(test_basename_dirname_eq)
     98      if is_os('win') then
     99        test_paths(tests_windows_paths, true)
    100      end
    101    end)
    102  end)
    103 
    104  describe('basename()', function()
    105    it('works', function()
    106      eq(nvim_prog_basename, vim.fs.basename(nvim_prog))
    107 
    108      ---@param paths string[]
    109      ---@param is_win? boolean
    110      local function test_paths(paths, is_win)
    111        local gsub = is_win and [[:gsub('\\', '/')]] or ''
    112        local code = string.format(
    113          [[
    114          local path = ...
    115          return vim.fn.fnamemodify(path,':t')%s
    116        ]],
    117          gsub
    118        )
    119 
    120        for _, path in ipairs(paths) do
    121          eq(exec_lua(code, path), vim.fs.basename(path), path)
    122        end
    123      end
    124 
    125      test_paths(test_basename_dirname_eq)
    126      if is_os('win') then
    127        test_paths(tests_windows_paths, true)
    128      end
    129    end)
    130  end)
    131 
    132  describe('dir()', function()
    133    before_each(function()
    134      mkdir('testd')
    135      mkdir('testd/a')
    136      mkdir('testd/a/b')
    137      mkdir('testd/a/b/c')
    138    end)
    139 
    140    after_each(function()
    141      rmdir('testd')
    142    end)
    143 
    144    it('works', function()
    145      eq(
    146        true,
    147        exec_lua(function()
    148          for name, type in vim.fs.dir(nvim_dir) do
    149            if name == nvim_prog_basename and type == 'file' then
    150              return true
    151            end
    152          end
    153          return false
    154        end)
    155      )
    156    end)
    157 
    158    it('works with opts.depth, opts.skip and opts.follow', function()
    159      io.open('testd/a1', 'w'):close()
    160      io.open('testd/b1', 'w'):close()
    161      io.open('testd/c1', 'w'):close()
    162      io.open('testd/a/a2', 'w'):close()
    163      io.open('testd/a/b2', 'w'):close()
    164      io.open('testd/a/c2', 'w'):close()
    165      io.open('testd/a/b/a3', 'w'):close()
    166      io.open('testd/a/b/b3', 'w'):close()
    167      io.open('testd/a/b/c3', 'w'):close()
    168      io.open('testd/a/b/c/a4', 'w'):close()
    169      io.open('testd/a/b/c/b4', 'w'):close()
    170      io.open('testd/a/b/c/c4', 'w'):close()
    171 
    172      local function run(dir, depth, skip, follow)
    173        return exec_lua(function(follow_)
    174          local r = {} --- @type table<string, string>
    175          local skip_f --- @type function
    176          if skip then
    177            skip_f = function(n0)
    178              if vim.tbl_contains(skip or {}, n0) then
    179                return false
    180              end
    181            end
    182          end
    183          for name, type_ in vim.fs.dir(dir, { depth = depth, skip = skip_f, follow = follow_ }) do
    184            r[name] = type_
    185          end
    186          return r
    187        end, follow)
    188      end
    189 
    190      local exp = {}
    191 
    192      exp['a1'] = 'file'
    193      exp['b1'] = 'file'
    194      exp['c1'] = 'file'
    195      exp['a'] = 'directory'
    196 
    197      eq(exp, run('testd', 1))
    198 
    199      exp['a/a2'] = 'file'
    200      exp['a/b2'] = 'file'
    201      exp['a/c2'] = 'file'
    202      exp['a/b'] = 'directory'
    203      local lexp = vim.deepcopy(exp)
    204 
    205      eq(exp, run('testd', 2))
    206 
    207      exp['a/b/a3'] = 'file'
    208      exp['a/b/b3'] = 'file'
    209      exp['a/b/c3'] = 'file'
    210      exp['a/b/c'] = 'directory'
    211 
    212      eq(exp, run('testd', 3))
    213      eq(exp, run('testd', 999, { 'a/b/c' }))
    214 
    215      exp['a/b/c/a4'] = 'file'
    216      exp['a/b/c/b4'] = 'file'
    217      exp['a/b/c/c4'] = 'file'
    218 
    219      eq(exp, run('testd', 999))
    220 
    221      vim.uv.fs_symlink(vim.uv.fs_realpath('testd/a'), 'testd/l', { junction = true, dir = true })
    222      lexp['l'] = 'link'
    223      eq(lexp, run('testd', 2, nil, false))
    224 
    225      lexp['l/a2'] = 'file'
    226      lexp['l/b2'] = 'file'
    227      lexp['l/c2'] = 'file'
    228      lexp['l/b'] = 'directory'
    229      eq(lexp, run('testd', 2, nil, true))
    230    end)
    231 
    232    it('follow=true handles symlink loop', function()
    233      local cwd = 'testd/a/b/c'
    234      local symlink = cwd .. '/link_loop' ---@type string
    235      vim.uv.fs_symlink(vim.uv.fs_realpath(cwd), symlink, { junction = true, dir = true })
    236 
    237      eq(
    238        link_limit,
    239        exec_lua(function()
    240          return #vim.iter(vim.fs.dir(cwd, { depth = math.huge, follow = true })):totable()
    241        end)
    242      )
    243    end)
    244  end)
    245 
    246  describe('find()', function()
    247    it('works', function()
    248      eq(
    249        { test_build_dir .. '/bin' },
    250        vim.fs.find('bin', { path = nvim_dir, upward = true, type = 'directory' })
    251      )
    252      eq({ nvim_prog }, vim.fs.find(nvim_prog_basename, { path = test_build_dir, type = 'file' }))
    253 
    254      local parent, name = nvim_dir:match('^(.*/)([^/]+)$')
    255      eq({ nvim_dir }, vim.fs.find(name, { path = parent, upward = true, type = 'directory' }))
    256    end)
    257 
    258    it('follows symlinks', function()
    259      local build_dir = test_build_dir ---@type string
    260      local symlink = test_source_path .. '/build_link' ---@type string
    261      vim.uv.fs_symlink(build_dir, symlink, { junction = true, dir = true })
    262 
    263      finally(function()
    264        vim.uv.fs_unlink(symlink)
    265      end)
    266 
    267      local cases = { nvim_prog, symlink .. '/bin/' .. nvim_prog_basename }
    268      table.sort(cases)
    269 
    270      eq(
    271        cases,
    272        vim.fs.find(nvim_prog_basename, {
    273          path = test_source_path,
    274          type = 'file',
    275          limit = 2,
    276          follow = true,
    277        })
    278      )
    279 
    280      if t.is_zig_build() then
    281        return pending('broken with build.zig')
    282      end
    283      eq(
    284        { nvim_prog },
    285        vim.fs.find(nvim_prog_basename, {
    286          path = test_source_path,
    287          type = 'file',
    288          limit = 2,
    289          follow = false,
    290        })
    291      )
    292    end)
    293 
    294    it('follow=true handles symlink loop', function()
    295      if t.is_zig_build() then
    296        return pending('broken/slow with build.zig')
    297      end
    298      local cwd = test_source_path ---@type string
    299      local symlink = test_source_path .. '/loop_link' ---@type string
    300      vim.uv.fs_symlink(cwd, symlink, { junction = true, dir = true })
    301 
    302      finally(function()
    303        vim.uv.fs_unlink(symlink)
    304      end)
    305 
    306      eq(link_limit, #vim.fs.find(nvim_prog_basename, {
    307        path = test_source_path,
    308        type = 'file',
    309        limit = math.huge,
    310        follow = true,
    311      }))
    312    end)
    313 
    314    it('accepts predicate as names', function()
    315      local opts = { path = nvim_dir, upward = true, type = 'directory' }
    316      eq(
    317        { test_build_dir .. '/bin' },
    318        vim.fs.find(function(x)
    319          return x == 'bin'
    320        end, opts)
    321      )
    322      eq(
    323        { nvim_prog },
    324        vim.fs.find(function(x)
    325          return x == nvim_prog_basename
    326        end, { path = test_build_dir, type = 'file' })
    327      )
    328      eq(
    329        {},
    330        vim.fs.find(function(x)
    331          return x == 'no-match'
    332        end, opts)
    333      )
    334 
    335      opts = { path = test_source_path .. '/contrib', limit = math.huge }
    336      eq(
    337        exec_lua(function()
    338          return vim.tbl_map(
    339            vim.fs.basename,
    340            vim.fn.glob(test_source_path .. '/contrib/*', false, true)
    341          )
    342        end),
    343        vim.tbl_map(
    344          vim.fs.basename,
    345          vim.fs.find(function(_, d)
    346            return d:match('[\\/]contrib$')
    347          end, opts)
    348        )
    349      )
    350    end)
    351  end)
    352 
    353  describe('root()', function()
    354    before_each(function()
    355      command('edit test/functional/fixtures/tty-test.c')
    356    end)
    357 
    358    after_each(function()
    359      command('bwipe!')
    360    end)
    361 
    362    it('works with a single marker', function()
    363      eq(test_source_path, exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
    364    end)
    365 
    366    it('works with multiple markers', function()
    367      local bufnr = api.nvim_get_current_buf()
    368      eq(
    369        vim.fs.joinpath(test_source_path, 'test/functional/fixtures'),
    370        exec_lua([[return vim.fs.root(..., {'CMakeLists.txt', 'CMakePresets.json'})]], bufnr)
    371      )
    372    end)
    373 
    374    it('nested markers have equal priority', function()
    375      local bufnr = api.nvim_get_current_buf()
    376      eq(
    377        vim.fs.joinpath(test_source_path, 'test/functional'),
    378        exec_lua(
    379          [[return vim.fs.root(..., { 'example_spec.lua', {'CMakeLists.txt', 'CMakePresets.json'}, '.luarc.json'})]],
    380          bufnr
    381        )
    382      )
    383      eq(
    384        vim.fs.joinpath(test_source_path, 'test/functional/fixtures'),
    385        exec_lua(
    386          [[return vim.fs.root(..., { {'CMakeLists.txt', 'CMakePresets.json'}, 'example_spec.lua', '.luarc.json'})]],
    387          bufnr
    388        )
    389      )
    390      eq(
    391        vim.fs.joinpath(test_source_path, 'test/functional/fixtures'),
    392        exec_lua(
    393          [[return vim.fs.root(..., {
    394            function(name, _)
    395              return name:match('%.txt$')
    396            end,
    397            'example_spec.lua',
    398            '.luarc.json' })]],
    399          bufnr
    400        )
    401      )
    402    end)
    403 
    404    it('works with a function', function()
    405      ---@type string
    406      local result = exec_lua(function()
    407        return vim.fs.root(0, function(name, _)
    408          return name:match('%.txt$')
    409        end)
    410      end)
    411      eq(vim.fs.joinpath(test_source_path, 'test/functional/fixtures'), result)
    412    end)
    413 
    414    it('works with a filename argument', function()
    415      eq(test_source_path, exec_lua([[return vim.fs.root(..., 'CMakePresets.json')]], nvim_prog))
    416    end)
    417 
    418    it('works with a relative path', function()
    419      eq(
    420        test_source_path,
    421        exec_lua([[return vim.fs.root(..., 'CMakePresets.json')]], vim.fs.basename(nvim_prog))
    422      )
    423    end)
    424 
    425    it('returns CWD (absolute path) for unnamed buffers', function()
    426      assert(n.fn.isabsolutepath(test_source_path) == 1)
    427      command('new')
    428      eq(
    429        t.fix_slashes(test_source_path),
    430        t.fix_slashes(exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
    431      )
    432    end)
    433 
    434    it("returns CWD (absolute path) for buffers with non-empty 'buftype'", function()
    435      assert(n.fn.isabsolutepath(test_source_path) == 1)
    436      command('new')
    437      command('set buftype=nofile')
    438      command('file lua://')
    439      eq(
    440        t.fix_slashes(test_source_path),
    441        t.fix_slashes(exec_lua([[return vim.fs.root(0, 'CMakePresets.json')]]))
    442      )
    443    end)
    444 
    445    it('returns CWD (absolute path) if no match is found', function()
    446      assert(n.fn.isabsolutepath(test_source_path) == 1)
    447      eq(
    448        t.fix_slashes(test_source_path),
    449        t.fix_slashes(exec_lua([[return vim.fs.root('file://bogus', 'CMakePresets.json')]]))
    450      )
    451    end)
    452  end)
    453 
    454  describe('joinpath()', function()
    455    it('works', function()
    456      eq('foo/bar/baz', vim.fs.joinpath('foo', 'bar', 'baz'))
    457      eq('foo/bar/baz', vim.fs.joinpath('foo', '/bar/', '/baz'))
    458    end)
    459    it('rewrites backslashes on Windows', function()
    460      if is_os('win') then
    461        eq('foo/bar/baz/zub/', vim.fs.joinpath([[foo]], [[\\bar\\\\baz]], [[zub\]]))
    462      else
    463        eq([[foo/\\bar\\\\baz/zub\]], vim.fs.joinpath([[foo]], [[\\bar\\\\baz]], [[zub\]]))
    464      end
    465    end)
    466    it('strips redundant slashes', function()
    467      if is_os('win') then
    468        eq('foo/bar/baz/zub/', vim.fs.joinpath([[foo//]], [[\\bar\\\\baz]], [[zub\]]))
    469      else
    470        eq('foo/bar/baz/zub/', vim.fs.joinpath([[foo]], [[//bar////baz]], [[zub/]]))
    471      end
    472    end)
    473    it('handles empty segments', function()
    474      eq('foo/bar', vim.fs.joinpath('', 'foo', '', 'bar', ''))
    475      eq('foo/bar', vim.fs.joinpath('', '', 'foo', 'bar', '', ''))
    476      eq('', vim.fs.joinpath(''))
    477      eq('', vim.fs.joinpath('', '', '', ''))
    478    end)
    479  end)
    480 
    481  describe('normalize()', function()
    482    it('removes trailing /', function()
    483      eq('/home/user', vim.fs.normalize('/home/user/'))
    484    end)
    485    it('works with /', function()
    486      eq('/', vim.fs.normalize('/'))
    487    end)
    488    it('works with ~', function()
    489      eq(vim.fs.normalize(assert(vim.uv.os_homedir())) .. '/src/foo', vim.fs.normalize('~/src/foo'))
    490    end)
    491    it('works with environment variables', function()
    492      local xdg_config_home = test_build_dir .. '/.config'
    493      eq(
    494        xdg_config_home .. '/nvim',
    495        exec_lua(function()
    496          return vim._with({ env = { XDG_CONFIG_HOME = xdg_config_home } }, function()
    497            return vim.fs.normalize('$XDG_CONFIG_HOME/nvim')
    498          end)
    499        end)
    500      )
    501    end)
    502 
    503    -- Opts required for testing posix paths and win paths
    504    local posix_opts = { win = false }
    505    local win_opts = { win = true }
    506 
    507    it('preserves leading double slashes in POSIX paths', function()
    508      eq('//foo', vim.fs.normalize('//foo', posix_opts))
    509      eq('//foo/bar', vim.fs.normalize('//foo//bar////', posix_opts))
    510      eq('/foo', vim.fs.normalize('///foo', posix_opts))
    511      eq('//', vim.fs.normalize('//', posix_opts))
    512      eq('/', vim.fs.normalize('///', posix_opts))
    513      eq('/foo/bar', vim.fs.normalize('/foo//bar////', posix_opts))
    514    end)
    515 
    516    it('normalizes drive letter', function()
    517      eq('C:/', vim.fs.normalize('C:/', win_opts))
    518      eq('C:/', vim.fs.normalize('c:/', win_opts))
    519      eq('D:/', vim.fs.normalize('d:/', win_opts))
    520      eq('C:', vim.fs.normalize('C:', win_opts))
    521      eq('C:', vim.fs.normalize('c:', win_opts))
    522      eq('D:', vim.fs.normalize('d:', win_opts))
    523      eq('C:/foo/test', vim.fs.normalize('C:/foo/test/', win_opts))
    524      eq('C:/foo/test', vim.fs.normalize('c:/foo/test/', win_opts))
    525      eq('D:foo/test', vim.fs.normalize('D:foo/test/', win_opts))
    526      eq('D:foo/test', vim.fs.normalize('d:foo/test/', win_opts))
    527    end)
    528 
    529    it('always treats paths as case-sensitive #31833', function()
    530      eq('TEST', vim.fs.normalize('TEST', win_opts))
    531      eq('test', vim.fs.normalize('test', win_opts))
    532      eq('C:/FOO/test', vim.fs.normalize('C:/FOO/test', win_opts))
    533      eq('C:/foo/test', vim.fs.normalize('C:/foo/test', win_opts))
    534      eq('//SERVER/SHARE/FOO/BAR', vim.fs.normalize('//SERVER/SHARE/FOO/BAR', win_opts))
    535      eq('//server/share/foo/bar', vim.fs.normalize('//server/share/foo/bar', win_opts))
    536      eq('C:/FOO/test', vim.fs.normalize('c:/FOO/test', win_opts))
    537    end)
    538 
    539    it('allows backslashes on unix-based os', function()
    540      eq('/home/user/hello\\world', vim.fs.normalize('/home/user/hello\\world', posix_opts))
    541    end)
    542 
    543    it('preserves / after drive letters', function()
    544      eq('C:/', vim.fs.normalize([[C:\]], win_opts))
    545    end)
    546 
    547    it('works with UNC and DOS device paths', function()
    548      eq('//server/share/foo/bar', vim.fs.normalize([[\\server\\share\\\foo\bar\\\]], win_opts))
    549      eq('//system07/C$/', vim.fs.normalize([[\\system07\C$\\\\]], win_opts))
    550      eq('//./C:/foo/bar', vim.fs.normalize([[\\.\\C:\foo\\\\bar]], win_opts))
    551      eq('//?/C:/foo/bar', vim.fs.normalize([[\\?\C:\\\foo\bar\\\\]], win_opts))
    552      eq(
    553        '//?/UNC/server/share/foo/bar',
    554        vim.fs.normalize([[\\?\UNC\server\\\share\\\\foo\\\bar]], win_opts)
    555      )
    556      eq('//./BootPartition/foo/bar', vim.fs.normalize([[\\.\BootPartition\\foo\bar]], win_opts))
    557      eq(
    558        '//./Volume{12345678-1234-1234-1234-1234567890AB}/foo/bar',
    559        vim.fs.normalize([[\\.\Volume{12345678-1234-1234-1234-1234567890AB}\\\foo\bar\\]], win_opts)
    560      )
    561    end)
    562 
    563    it('handles invalid UNC and DOS device paths', function()
    564      eq('//server/share', vim.fs.normalize([[\\server\share]], win_opts))
    565      eq('//server/', vim.fs.normalize([[\\server\]], win_opts))
    566      eq('//./UNC/server/share', vim.fs.normalize([[\\.\UNC\server\share]], win_opts))
    567      eq('//?/UNC/server/', vim.fs.normalize([[\\?\UNC\server\]], win_opts))
    568      eq('//?/UNC/server/..', vim.fs.normalize([[\\?\UNC\server\..]], win_opts))
    569      eq('//./', vim.fs.normalize([[\\.\]], win_opts))
    570      eq('//./foo', vim.fs.normalize([[\\.\foo]], win_opts))
    571      eq('//./BootPartition', vim.fs.normalize([[\\.\BootPartition]], win_opts))
    572    end)
    573 
    574    it('converts backward slashes', function()
    575      eq('C:/Users/jdoe', vim.fs.normalize([[C:\Users\jdoe]], win_opts))
    576    end)
    577 
    578    describe('. and .. component resolving', function()
    579      it('works', function()
    580        -- Windows paths
    581        eq('C:/Users', vim.fs.normalize([[C:\Users\jdoe\Downloads\.\..\..\]], win_opts))
    582        eq('C:/Users/jdoe', vim.fs.normalize([[C:\Users\jdoe\Downloads\.\..\.\.\]], win_opts))
    583        eq('C:/', vim.fs.normalize('C:/Users/jdoe/Downloads/./../../../', win_opts))
    584        eq('C:foo', vim.fs.normalize([[C:foo\bar\.\..\.]], win_opts))
    585        -- POSIX paths
    586        eq('/home', vim.fs.normalize('/home/jdoe/Downloads/./../..', posix_opts))
    587        eq('/home/jdoe', vim.fs.normalize('/home/jdoe/Downloads/./../././', posix_opts))
    588        eq('/', vim.fs.normalize('/home/jdoe/Downloads/./../../../', posix_opts))
    589        -- OS-agnostic relative paths
    590        eq('foo/bar/baz', vim.fs.normalize('foo/bar/foobar/../baz/./'))
    591        eq('foo/bar', vim.fs.normalize('foo/bar/foobar/../baz/./../../bar/./.'))
    592      end)
    593 
    594      it('works when relative path reaches current directory', function()
    595        eq('C:', vim.fs.normalize('C:foo/bar/../../.', win_opts))
    596 
    597        eq('.', vim.fs.normalize('.'))
    598        eq('.', vim.fs.normalize('././././'))
    599        eq('.', vim.fs.normalize('foo/bar/../../.'))
    600      end)
    601 
    602      it('works when relative path goes outside current directory', function()
    603        eq('../../foo/bar', vim.fs.normalize('../../foo/bar'))
    604        eq('../foo', vim.fs.normalize('foo/bar/../../../foo'))
    605 
    606        eq('C:../foo', vim.fs.normalize('C:../foo', win_opts))
    607        eq('C:../../foo/bar', vim.fs.normalize('C:foo/../../../foo/bar', win_opts))
    608      end)
    609 
    610      it('.. in root directory resolves to itself', function()
    611        eq('C:/', vim.fs.normalize('C:/../../', win_opts))
    612        eq('C:/foo', vim.fs.normalize('C:/foo/../../foo', win_opts))
    613 
    614        eq('//server/share/', vim.fs.normalize([[\\server\share\..\..]], win_opts))
    615        eq('//server/share/foo', vim.fs.normalize([[\\server\\share\foo\..\..\foo]], win_opts))
    616 
    617        eq('//./C:/', vim.fs.normalize([[\\.\C:\..\..]], win_opts))
    618        eq('//?/C:/foo', vim.fs.normalize([[\\?\C:\..\..\foo]], win_opts))
    619 
    620        eq('//./UNC/server/share/', vim.fs.normalize([[\\.\UNC\\server\share\..\..\]], win_opts))
    621        eq(
    622          '//?/UNC/server/share/foo',
    623          vim.fs.normalize([[\\?\UNC\server\\share\..\..\foo]], win_opts)
    624        )
    625 
    626        eq('//?/BootPartition/', vim.fs.normalize([[\\?\BootPartition\..\..]], win_opts))
    627        eq('//./BootPartition/foo', vim.fs.normalize([[\\.\BootPartition\..\..\foo]], win_opts))
    628 
    629        eq('/', vim.fs.normalize('/../../', posix_opts))
    630        eq('/foo', vim.fs.normalize('/foo/../../foo', posix_opts))
    631      end)
    632    end)
    633  end)
    634 
    635  describe('abspath()', function()
    636    local cwd = assert(t.fix_slashes(assert(vim.uv.cwd())))
    637    local home = t.fix_slashes(assert(vim.uv.os_homedir()))
    638 
    639    it('expands relative paths', function()
    640      assert(n.fn.isabsolutepath(cwd) == 1)
    641      eq(cwd, vim.fs.abspath('.'))
    642      eq(cwd .. '/foo', vim.fs.abspath('foo'))
    643      eq(cwd .. '/././foo', vim.fs.abspath('././foo'))
    644      eq(cwd .. '/.././../foo', vim.fs.abspath('.././../foo'))
    645    end)
    646 
    647    it('works with absolute paths', function()
    648      if is_os('win') then
    649        eq([[C:/foo]], vim.fs.abspath([[C:\foo]]))
    650        eq([[C:/foo/../.]], vim.fs.abspath([[C:\foo\..\.]]))
    651        eq('//foo/bar', vim.fs.abspath('\\\\foo\\bar'))
    652      else
    653        eq('/foo/../.', vim.fs.abspath('/foo/../.'))
    654        eq('/foo/bar', vim.fs.abspath('/foo/bar'))
    655      end
    656    end)
    657 
    658    it('expands ~', function()
    659      eq(home .. '/foo', vim.fs.abspath('~/foo'))
    660      eq(home .. '/./.././foo', vim.fs.abspath('~/./.././foo'))
    661    end)
    662 
    663    if is_os('win') then
    664      it('works with drive-specific cwd on Windows', function()
    665        local cwd_drive = cwd:match('^%w:')
    666 
    667        eq(cwd .. '/foo', vim.fs.abspath(cwd_drive .. 'foo'))
    668      end)
    669    end
    670  end)
    671 
    672  describe('relpath()', function()
    673    it('works', function()
    674      local cwd = assert(t.fix_slashes(assert(vim.uv.cwd())))
    675      local my_dir = vim.fs.joinpath(cwd, 'foo')
    676 
    677      eq(nil, vim.fs.relpath('/var/lib', '/var'))
    678      eq(nil, vim.fs.relpath('/var/lib', '/bin'))
    679      eq(nil, vim.fs.relpath(my_dir, 'bin'))
    680      eq(nil, vim.fs.relpath(my_dir, './bin'))
    681      eq(nil, vim.fs.relpath(my_dir, '././'))
    682      eq(nil, vim.fs.relpath(my_dir, '../'))
    683      eq(nil, vim.fs.relpath('/var/lib', '/'))
    684      eq(nil, vim.fs.relpath('/var/lib', '//'))
    685      eq(nil, vim.fs.relpath(' ', '/var'))
    686      eq(nil, vim.fs.relpath(' ', '/var'))
    687      eq('.', vim.fs.relpath('/var/lib', '/var/lib'))
    688      eq('lib', vim.fs.relpath('/var/', '/var/lib'))
    689      eq('var/lib', vim.fs.relpath('/', '/var/lib'))
    690      eq('bar/package.json', vim.fs.relpath('/foo/test', '/foo/test/bar/package.json'))
    691      eq('foo/bar', vim.fs.relpath(cwd, 'foo/bar'))
    692      eq('foo/bar', vim.fs.relpath('.', vim.fs.joinpath(cwd, 'foo/bar')))
    693      eq('bar', vim.fs.relpath('foo', 'foo/bar'))
    694      eq(nil, vim.fs.relpath('/var/lib', '/var/library/foo'))
    695 
    696      if is_os('win') then
    697        eq(nil, vim.fs.relpath('/', ' '))
    698        eq(nil, vim.fs.relpath('/', 'var'))
    699      else
    700        local cwd_rel_root = cwd:sub(2)
    701        eq(cwd_rel_root .. '/ ', vim.fs.relpath('/', ' '))
    702        eq(cwd_rel_root .. '/var', vim.fs.relpath('/', 'var'))
    703      end
    704 
    705      if is_os('win') then
    706        eq(nil, vim.fs.relpath('c:/aaaa/', '/aaaa/cccc'))
    707        eq(nil, vim.fs.relpath('c:/aaaa/', './aaaa/cccc'))
    708        eq(nil, vim.fs.relpath('c:/aaaa/', 'aaaa/cccc'))
    709        eq(nil, vim.fs.relpath('c:/blah\\blah', 'd:/games'))
    710        eq(nil, vim.fs.relpath('c:/games', 'd:/games'))
    711        eq(nil, vim.fs.relpath('c:/games', 'd:/games/foo'))
    712        eq(nil, vim.fs.relpath('c:/aaaa/bbbb', 'c:/aaaa'))
    713        eq('cccc', vim.fs.relpath('c:/aaaa/', 'c:/aaaa/cccc'))
    714        eq('aaaa/bbbb', vim.fs.relpath('C:/', 'c:\\aaaa\\bbbb'))
    715        eq('bar/package.json', vim.fs.relpath('C:\\foo\\test', 'C:\\foo\\test\\bar\\package.json'))
    716        eq('baz', vim.fs.relpath('\\\\foo\\bar', '\\\\foo\\bar\\baz'))
    717        eq(nil, vim.fs.relpath('a/b/c', 'a\\b'))
    718        eq('d', vim.fs.relpath('a/b/c', 'a\\b\\c\\d'))
    719        eq('.', vim.fs.relpath('\\\\foo\\bar\\baz', '\\\\foo\\bar\\baz'))
    720        eq(nil, vim.fs.relpath('C:\\foo\\test', 'C:\\foo\\Test\\bar\\package.json'))
    721      end
    722    end)
    723  end)
    724 
    725  describe('rm()', function()
    726    before_each(function()
    727      t.mkdir('Xtest_fs-rm')
    728      t.write_file('Xtest_fs-rm/file-to-link', 'File to link')
    729      t.mkdir('Xtest_fs-rm/dir-to-link')
    730      t.write_file('Xtest_fs-rm/dir-to-link/file', 'File in dir to link')
    731    end)
    732 
    733    after_each(function()
    734      vim.uv.fs_unlink('Xtest_fs-rm/dir-to-link/file')
    735      vim.uv.fs_rmdir('Xtest_fs-rm/dir-to-link')
    736      vim.uv.fs_unlink('Xtest_fs-rm/file-to-link')
    737      vim.uv.fs_rmdir('Xtest_fs-rm')
    738    end)
    739 
    740    it('symlink', function()
    741      -- File
    742      vim.uv.fs_symlink('Xtest_fs-rm/file-to-link', 'Xtest_fs-rm/file-as-link')
    743      vim.fs.rm('Xtest_fs-rm/file-as-link')
    744      eq(vim.uv.fs_stat('Xtest_fs-rm/file-as-link'), nil)
    745      eq({ 'File to link' }, fn.readfile('Xtest_fs-rm/file-to-link'))
    746 
    747      -- Directory
    748      local function assert_rm_symlinked_dir(opts)
    749        vim.uv.fs_symlink('Xtest_fs-rm/dir-to-link', 'Xtest_fs-rm/dir-as-link')
    750        vim.fs.rm('Xtest_fs-rm/dir-as-link', opts)
    751        eq(vim.uv.fs_stat('Xtest_fs-rm/dir-as-link'), nil)
    752        eq({ 'File in dir to link' }, fn.readfile('Xtest_fs-rm/dir-to-link/file'))
    753      end
    754 
    755      assert_rm_symlinked_dir({})
    756      assert_rm_symlinked_dir({ force = true })
    757      assert_rm_symlinked_dir({ recursive = true })
    758      assert_rm_symlinked_dir({ recursive = true, force = true })
    759    end)
    760  end)
    761 end)