mirror of
https://github.com/neovim/neovim.git
synced 2026-09-12 17:11:02 +00:00
Problem: Watcher failures are reported inconsistently, and callers cannot detect when a watcher is no longer usable. Solution: Route startup and event failures and unexpected inotifywait exits through an optional on_error callback. Log to nvim-watch.log by default. Ignore disappearing child directories and exits caused by cancellation. Let LSP log failures and notify once per message, using INFO for missing roots and ERROR otherwise, without interrupting registration. AI-assisted
247 lines
7.6 KiB
Lua
247 lines
7.6 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
|
|
local describe, it, before_each = t.describe, t.it, t.before_each
|
|
local eq = t.eq
|
|
local exec_lua = n.exec_lua
|
|
local clear = n.clear
|
|
local is_ci = t.is_ci
|
|
local is_os = t.is_os
|
|
local skip = t.skip
|
|
|
|
-- Create a file via a rename to avoid multiple
|
|
-- events which can happen with some backends on some platforms
|
|
local function touch(path)
|
|
local tmp = t.tmpname()
|
|
assert(vim.uv.fs_rename(tmp, path))
|
|
end
|
|
|
|
describe('vim._watch', function()
|
|
before_each(function()
|
|
clear()
|
|
end)
|
|
|
|
it('watchdirs() does not scan excluded subtrees', function()
|
|
local root_dir = t.tmpname(false)
|
|
t.finally(function()
|
|
n.rmdir(root_dir)
|
|
end)
|
|
n.mkdir_p(root_dir .. '/src/deep')
|
|
n.mkdir_p(root_dir .. '/node_modules/pkg/excluded/deep')
|
|
|
|
-- Also cover a pattern that matches the directory but not its descendants.
|
|
for _, pattern in ipairs({ '**/node_modules/*/**', '**/excluded' }) do
|
|
local scanned = exec_lua(function(root, exclude_pattern)
|
|
root = vim.fs.normalize(root)
|
|
local scanned = {}
|
|
local fs_scandir = vim.uv.fs_scandir
|
|
vim.uv.fs_scandir = function(path, ...)
|
|
scanned[#scanned + 1] = path:sub(#root + 1)
|
|
return fs_scandir(path, ...)
|
|
end
|
|
local cancel = vim._watch.watchdirs(root, {
|
|
exclude_pattern = vim.glob.to_lpeg(exclude_pattern),
|
|
}, function() end)
|
|
vim.uv.fs_scandir = fs_scandir
|
|
cancel()
|
|
table.sort(scanned)
|
|
return scanned
|
|
end, root_dir, pattern)
|
|
|
|
eq({ '', '/node_modules', '/node_modules/pkg', '/src', '/src/deep' }, scanned, pattern)
|
|
end
|
|
end)
|
|
|
|
it('watchdirs() tolerates directories deleted during setup', function()
|
|
local root_dir = t.tmpname(false)
|
|
t.finally(function()
|
|
n.rmdir(root_dir)
|
|
end)
|
|
n.mkdir_p(root_dir .. '/gone')
|
|
|
|
exec_lua(function(root)
|
|
local dir = vim.fs.dir
|
|
vim.fs.dir = function(path, opts)
|
|
local iter = dir(path, opts)
|
|
return function()
|
|
local name, kind = iter()
|
|
if name == 'gone' then
|
|
-- Delete after enumeration, before the backend starts watching it.
|
|
assert(vim.uv.fs_rmdir(root .. '/gone'))
|
|
end
|
|
return name, kind
|
|
end
|
|
end
|
|
local cancel = vim._watch.watchdirs(root, { on_error = error }, function() end)
|
|
vim.fs.dir = dir
|
|
cancel()
|
|
end, root_dir)
|
|
end)
|
|
|
|
it('inotify() reports failure to start the process', function()
|
|
exec_lua(function()
|
|
vim.env.PATH = ''
|
|
local errors = {}
|
|
local cancel = vim._watch.inotify('.', {
|
|
on_error = function(err)
|
|
errors[#errors + 1] = err
|
|
end,
|
|
}, function() end)
|
|
cancel()
|
|
assert(#errors == 1, vim.inspect(errors))
|
|
assert(errors[1]:find('ENOENT', 1, true), errors[1])
|
|
assert(vim._watch.active.inotify == 0)
|
|
end)
|
|
end)
|
|
|
|
local function run(watchfunc)
|
|
local function do_watch(root_dir, watchfunc_)
|
|
exec_lua(
|
|
[[
|
|
local root_dir, watchfunc = ...
|
|
|
|
_G.events = {}
|
|
|
|
_G.stop_watch = vim._watch[watchfunc](root_dir, {
|
|
debounce = 100,
|
|
include_pattern = vim.lpeg.P(root_dir) * vim.lpeg.P("/file") ^ -1,
|
|
exclude_pattern = vim.lpeg.P(root_dir .. '/file.unwatched'),
|
|
}, function(path, change_type)
|
|
table.insert(_G.events, { path = path, change_type = change_type })
|
|
end)
|
|
]],
|
|
root_dir,
|
|
watchfunc_
|
|
)
|
|
end
|
|
|
|
it(watchfunc .. '() reports nonexistent paths to on_error', function()
|
|
if watchfunc == 'inotify' then
|
|
skip(n.fn.executable('inotifywait') == 0, 'inotifywait not found')
|
|
skip(is_os('bsd'), 'inotifywait on bsd CI seems to expect path to exist?')
|
|
skip(t.is_arch('s390x'), 'inotifywait not available on s390x CI')
|
|
end
|
|
|
|
exec_lua(function(backend)
|
|
local errors = {}
|
|
local cancel = vim._watch[backend]('/i am /very/funny.go', {
|
|
on_error = function(err)
|
|
errors[#errors + 1] = err
|
|
end,
|
|
}, function()
|
|
error('Unexpected file change')
|
|
end)
|
|
assert(vim.wait(2000, function()
|
|
return #errors > 0
|
|
end))
|
|
if backend ~= 'inotify' then
|
|
assert(errors[1]:match('^ENOENT:'), errors[1])
|
|
end
|
|
cancel()
|
|
end, watchfunc)
|
|
end)
|
|
|
|
if watchfunc ~= 'inotify' then
|
|
it(watchfunc .. '() logs startup failures without on_error', function()
|
|
local logfile = exec_lua(function(backend)
|
|
local logfile = vim.fs.joinpath(vim.fn.stdpath('log'), 'nvim-watch.log')
|
|
vim.fn.writefile({}, logfile)
|
|
local cancel = vim._watch[backend]('/i am /very/funny.go', {}, function()
|
|
error('Unexpected file change')
|
|
end)
|
|
cancel()
|
|
assert(vim._watch.active[backend] == 0)
|
|
return logfile
|
|
end, watchfunc)
|
|
t.assert_log('%[ERROR%].-ENOENT:', logfile)
|
|
end)
|
|
end
|
|
|
|
it(watchfunc .. '() detects file changes', function()
|
|
if watchfunc == 'inotify' then
|
|
skip(is_os('win'), 'N/A: inotify not supported on Windows')
|
|
skip(is_os('mac'), 'flaky test on mac')
|
|
skip(not is_ci() and n.fn.executable('inotifywait') == 0, 'inotifywait not found')
|
|
skip(t.is_arch('s390x'), 'inotifywait not available on s390x CI')
|
|
end
|
|
|
|
-- Note: because this is not `elseif`, BSD is skipped for *all* cases...?
|
|
if watchfunc == 'watch' then
|
|
skip(is_os('mac'), 'flaky test on mac')
|
|
skip(is_os('bsd'), 'Stopped working on bsd after 3ca967387c49c754561c3b11a574797504d40f38')
|
|
elseif watchfunc == 'watchdirs' and is_os('mac') then
|
|
skip(true, 'weird failure since macOS 14 CI, see bbf208784ca279178ba0075b60d3e9c80f11da7a')
|
|
else
|
|
skip(
|
|
is_os('bsd'),
|
|
'kqueue only reports events on watched folder itself, not contained files #26110'
|
|
)
|
|
end
|
|
|
|
local expected_events = 0
|
|
--- Waits for a new event, or fails if no events are triggered.
|
|
local function wait_for_event()
|
|
expected_events = expected_events + 1
|
|
exec_lua(
|
|
[[
|
|
local expected_events = ...
|
|
assert(
|
|
vim.wait(3000, function()
|
|
return #_G.events == expected_events
|
|
end),
|
|
string.format(
|
|
'Timed out waiting for expected event no. %d. Current events seen so far: %s',
|
|
expected_events,
|
|
vim.inspect(events)
|
|
)
|
|
)
|
|
]],
|
|
expected_events
|
|
)
|
|
end
|
|
|
|
local root_dir = vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX')
|
|
local unwatched_path = root_dir .. '/file.unwatched'
|
|
local watched_path = root_dir .. '/file'
|
|
|
|
do_watch(root_dir, watchfunc)
|
|
|
|
if watchfunc ~= 'watch' then
|
|
vim.uv.sleep(200)
|
|
end
|
|
|
|
touch(watched_path)
|
|
touch(unwatched_path)
|
|
wait_for_event()
|
|
|
|
os.remove(watched_path)
|
|
os.remove(unwatched_path)
|
|
wait_for_event()
|
|
|
|
exec_lua [[_G.stop_watch()]]
|
|
-- No events should come through anymore
|
|
|
|
vim.uv.sleep(100)
|
|
touch(watched_path)
|
|
vim.uv.sleep(100)
|
|
os.remove(watched_path)
|
|
vim.uv.sleep(100)
|
|
|
|
eq({
|
|
{
|
|
change_type = exec_lua([[return vim._watch.FileChangeType.Created]]),
|
|
path = root_dir .. '/file',
|
|
},
|
|
{
|
|
change_type = exec_lua([[return vim._watch.FileChangeType.Deleted]]),
|
|
path = root_dir .. '/file',
|
|
},
|
|
}, exec_lua [[return _G.events]])
|
|
end)
|
|
end
|
|
|
|
run('watch')
|
|
run('watchdirs')
|
|
run('inotify')
|
|
end)
|