aboutsummaryrefslogtreecommitdiff
path: root/test/functional/lua/fs_spec.lua
diff options
context:
space:
mode:
authorRudrajeet Pal <rudrajeetpal64@gmail.com>2026-06-04 16:33:03 +0530
committerJustin M. Keyes <justinkz@gmail.com>2026-07-01 12:05:41 +0200
commit4e04dff2287eed576cdb1063d5b7c00bf91b9fad (patch)
treec0f6733b71c81bbf34307abfbcb8288bab49c5f7 /test/functional/lua/fs_spec.lua
parent0d81f257afd24373144873e8f030bf929c1900e2 (diff)
downloadrneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.tar.gz
rneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.tar.bz2
rneovim-4e04dff2287eed576cdb1063d5b7c00bf91b9fad.zip
feat(vim.fs): dir(), find() error-reporting
Problem: vim.fs.dir() and vim.fs.find() drop errors returned by uv.fs_scandir(). Solution: - vim.fs.dir(): - Return root scan failures as a secondary return value. - Propagate recursive scan failures through the iterator. This allows callers to distinguish unreadable directories from empty ones. - vim.fs.find(): Collect errors during search, and return the list as a second retval.
Diffstat (limited to 'test/functional/lua/fs_spec.lua')
-rw-r--r--test/functional/lua/fs_spec.lua159
1 files changed, 159 insertions, 0 deletions
diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua
index 8434a36373..302d1445ac 100644
--- a/test/functional/lua/fs_spec.lua
+++ b/test/functional/lua/fs_spec.lua
@@ -287,6 +287,58 @@ describe('vim.fs', function()
eq('file', result['test.txt'])
end)
end)
+
+ describe('error handling', function()
+ before_each(function()
+ mkdir('testdir')
+ mkdir('testdir/noaccess')
+ mkdir('testdir/a')
+ mkdir('testdir/a/noaccess')
+ end)
+ after_each(function()
+ rmdir('testdir')
+ end)
+
+ it('returns error for unreadable root directory', function()
+ local result = exec_lua(function()
+ local _, err = vim.fs.dir('does-not-exist')
+ return err
+ end)
+ eq('ENOENT: no such file or directory: does-not-exist', result)
+ end)
+
+ it('returns iterator errors for unreadable child directories', function()
+ local result = exec_lua(function()
+ -- Stub fs_scandir since chmod doesn't work reliably on Windows.
+ local orig_scandir = vim.uv.fs_scandir
+ vim.uv.fs_scandir = function(path, ...)
+ if path == 'testdir/noaccess' then
+ return nil, 'EACCES: permission denied: testdir/noaccess'
+ end
+ return orig_scandir(path, ...)
+ end
+
+ local ok, errors_or_err = pcall(function()
+ local errors = {} ---@type table<string, string>
+ local it = vim.fs.dir('testdir', { depth = 2 })
+ for f, _, err in it do
+ errors[f] = err
+ end
+ return errors
+ end)
+
+ vim.uv.fs_scandir = orig_scandir
+
+ if not ok then
+ error(errors_or_err)
+ end
+ return errors_or_err
+ end)
+ eq('EACCES: permission denied: testdir/noaccess', result['noaccess'])
+ -- This should be nil because with a depth of 2 we shouldn't be scanning it
+ eq(nil, result['a/noaccess'])
+ end)
+ end)
end)
describe('find()', function()
@@ -394,6 +446,113 @@ describe('vim.fs', function()
)
)
end)
+
+ describe('error handling', function()
+ before_each(function()
+ mkdir('testdir')
+ mkdir('testdir/noaccess')
+ mkdir('testdir/a')
+ mkdir('testdir/a/noaccess')
+ t.write_file('testdir/a/match.lua', '')
+
+ exec_lua(function()
+ -- Stub fs_scandir since chmod doesn't work reliably on Windows.
+ local blocked_prefixes = { 'testdir/noaccess', 'testdir/a/noaccess' }
+ local function is_blocked(path)
+ for _, prefix in ipairs(blocked_prefixes) do
+ if path == prefix or vim.startswith(path, prefix .. '/') then
+ return true
+ end
+ end
+ return false
+ end
+ _G.__orig_fs_scandir = vim.uv.fs_scandir
+ vim.uv.fs_scandir = function(path, ...)
+ if is_blocked(path) then
+ return nil, 'EACCES: permission denied: ' .. path
+ end
+ return _G.__orig_fs_scandir(path, ...)
+ end
+ end)
+ end)
+
+ after_each(function()
+ exec_lua(function()
+ vim.uv.fs_scandir = _G.__orig_fs_scandir
+ _G.__orig_fs_scandir = nil
+ end)
+ rmdir('testdir')
+ end)
+
+ it('returns an error for a nonexistent root path', function()
+ local result = exec_lua(function()
+ local matches, errors = vim.fs.find('foo', { path = 'does-not-exist' })
+ return { matches = matches, errors = errors }
+ end)
+ eq({}, result.matches)
+ eq(1, #result.errors)
+ eq('does-not-exist', result.errors[1].path)
+ eq('ENOENT: no such file or directory: does-not-exist', result.errors[1].err)
+ end)
+
+ it('returns an error for an unreadable root path', function()
+ local result = exec_lua(function()
+ local matches, errors = vim.fs.find('foo', { path = 'testdir/noaccess' })
+ return { matches = matches, errors = errors }
+ end)
+ eq({}, result.matches)
+ eq(1, #result.errors)
+ eq('testdir/noaccess', result.errors[1].path)
+ eq('EACCES: permission denied: testdir/noaccess', result.errors[1].err)
+ end)
+
+ it('collects errors for unreadable child directories during downward search', function()
+ local result = exec_lua(function()
+ local matches, errors = vim.fs.find('match.lua', {
+ path = 'testdir',
+ limit = math.huge,
+ type = 'file',
+ })
+ --- @type table<string, string>
+ local err_by_path = {}
+ for _, e in ipairs(errors) do
+ err_by_path[e.path] = e.err
+ end
+ return { matches = matches, errors = err_by_path }
+ end)
+ eq(1, #result.matches)
+ eq('EACCES: permission denied: testdir/noaccess', result.errors['testdir/noaccess'])
+ eq('EACCES: permission denied: testdir/a/noaccess', result.errors['testdir/a/noaccess'])
+ end)
+
+ it('still returns matches found despite errors elsewhere in the tree', function()
+ local result = exec_lua(function()
+ local matches, errors = vim.fs.find('match.lua', {
+ path = 'testdir',
+ limit = math.huge,
+ type = 'file',
+ })
+ return { matches = matches, has_errors = #errors > 0 }
+ end)
+ eq(1, #result.matches)
+ eq(true, result.has_errors)
+ end)
+
+ it('reports errors for unreadable directories during upward search', function()
+ local result = exec_lua(function()
+ local matches, errors = vim.fs.find('match.lua', {
+ path = 'testdir/noaccess/x',
+ upward = true,
+ stop = 'testdir',
+ })
+ return { matches = matches, errors = errors }
+ end)
+ eq('EACCES: permission denied: testdir/noaccess/x', result.errors[1].err)
+ eq('EACCES: permission denied: testdir/noaccess', result.errors[2].err)
+ eq('testdir/noaccess/x', result.errors[1].path)
+ eq('testdir/noaccess', result.errors[2].path)
+ end)
+ end)
end)
describe('root()', function()