local t = require('test.testutil') local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1' local api = n.api local fn = n.fn local eq = t.eq local matches = t.matches local pcall_err = t.pcall_err local exec_lua = n.exec_lua -- Helpers ==================================================================== -- Installed plugins ---------------------------------------------------------- local function pack_get_dir() return vim.fs.joinpath(fn.stdpath('data'), 'site', 'pack', 'core', 'opt') end local function pack_get_plug_path(plug_name) return vim.fs.joinpath(pack_get_dir(), plug_name) end local function pack_exists(plug_name) local path = vim.fs.joinpath(pack_get_dir(), plug_name) return vim.uv.fs_stat(path) ~= nil end -- Test repos (to be installed) ----------------------------------------------- local repos_dir = vim.fs.abspath('test/functional/lua/pack-test-repos') --- Map from repo name to its proper `src` used in plugin spec --- @type table local repos_src = {} local function repo_get_path(repo_name) vim.validate('repo_name', repo_name, 'string') return vim.fs.joinpath(repos_dir, repo_name) end local function repo_write_file(repo_name, rel_path, text, no_dedent, append) local path = vim.fs.joinpath(repo_get_path(repo_name), rel_path) fn.mkdir(vim.fs.dirname(path), 'p') t.write_file(path, text, no_dedent, append) end --- @return vim.SystemCompleted local function system_sync(cmd, opts) return exec_lua(function() local obj = vim.system(cmd, opts) if opts and opts.timeout then -- Minor delay before calling wait() so the timeout uv timer can have a headstart over the -- internal call to vim.wait() in wait(). vim.wait(10) end local res = obj:wait() -- Check the process is no longer running assert(not vim.api.nvim_get_proc(obj.pid), 'process still exists') return res end) end local function git_cmd(cmd, repo_name) local git_cmd_prefix = { 'git', '-c', 'gc.auto=0', '-c', 'user.name=Marvim', '-c', 'user.email=marvim@neovim.io', '-c', 'init.defaultBranch=main', } cmd = vim.list_extend(git_cmd_prefix, cmd) local cwd = repo_get_path(repo_name) local sys_opts = { cwd = cwd, text = true, clear_env = true } local out = system_sync(cmd, sys_opts) if out.code ~= 0 then error(out.stderr) end return (out.stdout:gsub('\n+$', '')) end local function init_test_repo(repo_name) local path = repo_get_path(repo_name) fn.mkdir(path, 'p') repos_src[repo_name] = 'file://' .. path git_cmd({ 'init' }, repo_name) end local function git_add_commit(msg, repo_name) git_cmd({ 'add', '*' }, repo_name) git_cmd({ 'commit', '-m', msg }, repo_name) end local function git_get_hash(rev, repo_name) return git_cmd({ 'rev-list', '-1', '--abbrev-commit', rev }, repo_name) end -- Common test repos ---------------------------------------------------------- --- @type table local repos_setup = {} function repos_setup.basic() init_test_repo('basic') repo_write_file('basic', 'lua/basic.lua', 'return "basic init"') git_add_commit('Initial commit for "basic"', 'basic') repo_write_file('basic', 'lua/basic.lua', 'return "basic main"') git_add_commit('Commit in `main` but not in `feat-branch`', 'basic') git_cmd({ 'checkout', 'main~' }, 'basic') git_cmd({ 'checkout', '-b', 'feat-branch' }, 'basic') repo_write_file('basic', 'lua/basic.lua', 'return "basic some-tag"') git_add_commit('Add commit for some tag', 'basic') git_cmd({ 'tag', 'some-tag' }, 'basic') repo_write_file('basic', 'lua/basic.lua', 'return "basic feat-branch"') git_add_commit('Add important feature', 'basic') -- Make sure that `main` is the default remote branch git_cmd({ 'checkout', 'main' }, 'basic') end function repos_setup.plugindirs() init_test_repo('plugindirs') repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs main"') repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true') repo_write_file('plugindirs', 'plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "p"') repo_write_file('plugindirs', 'plugin/dirs.vim', 'let g:_plugin_vim=v:true') repo_write_file('plugindirs', 'plugin/sub/dirs.lua', 'vim.g._plugin_sub = true') repo_write_file('plugindirs', 'plugin/bad % name.lua', 'vim.g._plugin_bad = true') repo_write_file('plugindirs', 'after/plugin/dirs.lua', 'vim.g._after_plugin = true') repo_write_file('plugindirs', 'after/plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "a"') repo_write_file('plugindirs', 'after/plugin/dirs.vim', 'let g:_after_plugin_vim=v:true') repo_write_file('plugindirs', 'after/plugin/sub/dirs.lua', 'vim.g._after_plugin_sub = true') repo_write_file('plugindirs', 'after/plugin/bad % name.lua', 'vim.g._after_plugin_bad = true') git_add_commit('Initial commit for "plugindirs"', 'plugindirs') end function repos_setup.helptags() init_test_repo('helptags') repo_write_file('helptags', 'lua/helptags.lua', 'return "helptags main"') repo_write_file('helptags', 'doc/my-test-help.txt', '*my-test-help*') repo_write_file('helptags', 'doc/bad % name.txt', '*my-test-help-bad*') repo_write_file('helptags', 'doc/bad % dir/file.txt', '*my-test-help-sub-bad*') git_add_commit('Initial commit for "helptags"', 'helptags') end function repos_setup.pluginerr() init_test_repo('pluginerr') repo_write_file('pluginerr', 'lua/pluginerr.lua', 'return "pluginerr main"') repo_write_file('pluginerr', 'plugin/err.lua', 'error("Wow, an error")') git_add_commit('Initial commit for "pluginerr"', 'pluginerr') end function repos_setup.defbranch() init_test_repo('defbranch') repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch main"') git_add_commit('Initial commit for "defbranch"', 'defbranch') -- Make `dev` the default remote branch git_cmd({ 'checkout', '-b', 'dev' }, 'defbranch') repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch dev"') git_add_commit('Add to new default branch', 'defbranch') end function repos_setup.gitsuffix() init_test_repo('gitsuffix.git') repo_write_file('gitsuffix.git', 'lua/gitsuffix.lua', 'return "gitsuffix main"') git_add_commit('Initial commit for "gitsuffix"', 'gitsuffix.git') end function repos_setup.semver() init_test_repo('semver') local add_tag = function(name) repo_write_file('semver', 'lua/semver.lua', 'return "semver ' .. name .. '"') git_add_commit('Add version ' .. name, 'semver') git_cmd({ 'tag', name }, 'semver') end add_tag('v0.0.1') add_tag('v0.0.2') add_tag('v0.1.0') add_tag('v0.1.1') add_tag('v0.2.0-dev') add_tag('v0.2.0') add_tag('v0.3.0') repo_write_file('semver', 'lua/semver.lua', 'return "semver middle-commit') git_add_commit('Add middle commit', 'semver') add_tag('0.3.1') add_tag('v0.4') add_tag('non-semver') add_tag('v0.2.1') -- Intentionally add version not in order add_tag('v1.0.0') end -- Utility -------------------------------------------------------------------- local function watch_events(event) exec_lua(function() _G.event_log = _G.event_log or {} --- @type table[] vim.api.nvim_create_autocmd(event, { callback = function(ev) table.insert(_G.event_log, { event = ev.event, match = ev.match, data = ev.data }) end, }) end) end --- @param log table[] local function find_in_log(log, event, kind, repo_name, version) local path = pack_get_plug_path(repo_name) local spec = { name = repo_name, src = repos_src[repo_name], version = version } local data = { kind = kind, path = path, spec = spec } local entry = { event = event, match = vim.fs.abspath(path), data = data } local res = 0 for i, tbl in ipairs(log) do if vim.deep_equal(tbl, entry) then res = i break end end eq(true, res > 0) return res end local function validate_progress_report(title, step_names) -- NOTE: Assumes that message history contains only progress report messages local messages = vim.split(n.exec_capture('messages'), '\n') local n_steps = #step_names eq(n_steps + 2, #messages) local init_msg = ('vim.pack: 0%% %s (0/%d)'):format(title, n_steps) eq(init_msg, messages[1]) local steps_seen = {} --- @type table for i = 1, n_steps do local percent = math.floor(100 * i / n_steps) local msg = ('vim.pack: %3d%% %s (%d/%d)'):format(percent, title, i, n_steps) -- NOTE: There is no guaranteed order (as it is async), so check that some -- expected step name is used local pattern = '^' .. vim.pesc(msg) .. ' %- (%S+)$' local step = messages[i + 1]:match(pattern) eq(true, vim.tbl_contains(step_names, step)) steps_seen[step] = true end -- Should report all steps eq(n_steps, vim.tbl_count(steps_seen)) local final_msg = ('vim.pack: done %s (%d/%d)'):format(title, n_steps, n_steps) eq(final_msg, messages[n_steps + 2]) end local function is_jit() return exec_lua('return package.loaded.jit ~= nil') end -- Tests ====================================================================== describe('vim.pack', function() setup(function() n.clear() for _, r_setup in pairs(repos_setup) do r_setup() end end) before_each(function() n.clear() end) after_each(function() vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) end) teardown(function() vim.fs.rm(repos_dir, { force = true, recursive = true }) end) describe('add()', function() it('installs only once', function() exec_lua(function() vim.pack.add({ repos_src.basic }) end) n.clear() watch_events({ 'PackChanged' }) exec_lua(function() vim.pack.add({ repos_src.basic }) end) eq(exec_lua('return #_G.event_log'), 0) end) it('passes data field through to opts.load', function() eq( 2, exec_lua(function() local successes = 0 vim.pack.add({ { name = 'tabletest', src = repos_src.basic, data = { test = 'value' } }, { name = 'stringtest', src = repos_src.basic, data = 'value' }, }, { confirm = false, load = function(p) if p.spec.name == 'tabletest' then if p.spec.data.test == 'value' then successes = successes + 1 end end if p.spec.name == 'stringtest' then if p.spec.data == 'value' then successes = successes + 1 end end end, }) return successes end) ) end) it('asks for installation confirmation', function() exec_lua(function() ---@diagnostic disable-next-line: duplicate-set-field vim.fn.confirm = function(...) _G.confirm_args = { ... } -- Do not confirm installation to see what happens return 0 end end) local err = pcall_err(exec_lua, function() vim.pack.add({ repos_src.basic }) end) matches('`basic`:\nInstallation was not confirmed', err) eq(false, exec_lua('return pcall(require, "basic")')) local confirm_msg = 'These plugins will be installed:\n\n' .. repos_src.basic .. '\n' eq({ confirm_msg, 'Proceed? &Yes\n&No', 1, 'Question' }, exec_lua('return _G.confirm_args')) end) it('respects `opts.confirm`', function() exec_lua(function() _G.confirm_used = false ---@diagnostic disable-next-line: duplicate-set-field vim.fn.confirm = function() _G.confirm_used = true return 1 end vim.pack.add({ repos_src.basic }, { confirm = false }) end) eq(false, exec_lua('return _G.confirm_used')) eq('basic main', exec_lua('return require("basic")')) end) it('installs at proper version', function() local out = exec_lua(function() vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' }, }) -- Should have plugin available immediately after return require('basic') end) eq('basic feat-branch', out) local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths()) local plug_path = pack_get_plug_path('basic') local after_dir = vim.fs.joinpath(plug_path, 'after') eq(true, vim.tbl_contains(rtp, plug_path)) -- No 'after/' directory in runtimepath because it is not present in plugin eq(false, vim.tbl_contains(rtp, after_dir)) end) it('can install from the Internet', function() t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') exec_lua(function() vim.pack.add({ 'https://github.com/neovim/nvim-lspconfig' }) end) eq(true, exec_lua('return pcall(require, "lspconfig")')) end) describe('startup', function() local init_lua = '' before_each(function() init_lua = vim.fs.joinpath(fn.stdpath('config'), 'init.lua') fn.mkdir(vim.fs.dirname(init_lua), 'p') end) after_each(function() pcall(vim.fs.rm, init_lua, { force = true }) end) it('works in init.lua', function() local pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs)) fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua) local validate_loaded = function() eq('plugindirs main', exec_lua('return require("plugindirs")')) -- Should source 'plugin/' and 'after/plugin/' exactly once eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) eq({ 'p', 'a' }, n.exec_lua('return _G.DL')) end -- Should auto-install but wait before executing code after it n.clear({ args_rm = { '-u' } }) n.exec_lua('vim.wait(500, function() return _G.done end, 50)') validate_loaded() -- Should only `:packadd!` already installed plugin n.clear({ args_rm = { '-u' } }) validate_loaded() -- Should not load plugins if `--noplugin`, only adjust 'runtimepath' n.clear({ args = { '--noplugin' }, args_rm = { '-u' } }) eq('plugindirs main', exec_lua('return require("plugindirs")')) eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) eq(vim.NIL, n.exec_lua('return _G.DL')) end) end) it('shows progress report during installation', function() exec_lua(function() vim.pack.add({ repos_src.basic, repos_src.defbranch }) end) validate_progress_report('Installing plugins', { 'basic', 'defbranch' }) end) it('triggers relevant events', function() watch_events({ 'PackChangedPre', 'PackChanged' }) exec_lua(function() -- Should provide event-data respecting manual and inferred default `version` vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch }) end) local log = exec_lua('return _G.event_log') local installpre_basic = find_in_log(log, 'PackChangedPre', 'install', 'basic', 'feat-branch') local installpre_defbranch = find_in_log(log, 'PackChangedPre', 'install', 'defbranch', nil) local updatepre_basic = find_in_log(log, 'PackChangedPre', 'update', 'basic', 'feat-branch') local updatepre_defbranch = find_in_log(log, 'PackChangedPre', 'update', 'defbranch', 'dev') local update_basic = find_in_log(log, 'PackChanged', 'update', 'basic', 'feat-branch') local update_defbranch = find_in_log(log, 'PackChanged', 'update', 'defbranch', 'dev') local install_basic = find_in_log(log, 'PackChanged', 'install', 'basic', 'feat-branch') local install_defbranch = find_in_log(log, 'PackChanged', 'install', 'defbranch', 'dev') eq(8, #log) -- NOTE: There is no guaranteed installation order among separate plugins (as it is async) eq(true, installpre_basic < updatepre_basic) eq(true, updatepre_basic < update_basic) -- NOTE: "Install" is after "update" to indicate installation at correct version eq(true, update_basic < install_basic) eq(true, installpre_defbranch < updatepre_defbranch) eq(true, updatepre_defbranch < update_defbranch) eq(true, update_defbranch < install_defbranch) end) it('recognizes several `version` types', function() local prev_commit = git_get_hash('HEAD~', 'defbranch') exec_lua(function() vim.pack.add({ { src = repos_src.basic, version = 'some-tag' }, -- Tag { src = repos_src.defbranch, version = prev_commit }, -- Commit hash { src = repos_src.semver, version = vim.version.range('<1') }, -- Semver constraint }) end) eq('basic some-tag', exec_lua('return require("basic")')) eq('defbranch main', exec_lua('return require("defbranch")')) eq('semver v0.4', exec_lua('return require("semver")')) end) it('respects plugin/ and after/plugin/ scripts', function() local function validate(load, ref) local opts = { load = load } local out = exec_lua(function() -- Should handle bad plugin directory name vim.pack.add({ { src = repos_src.plugindirs, name = 'plugin % dirs' } }, opts) return { vim.g._plugin, vim.g._plugin_vim, vim.g._plugin_sub, vim.g._plugin_bad, vim.g._after_plugin, vim.g._after_plugin_vim, vim.g._after_plugin_sub, vim.g._after_plugin_bad, } end) eq(ref, out) -- Should add necessary directories to runtimepath regardless of `opts.load` local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths()) local plug_path = pack_get_plug_path('plugin % dirs') local after_dir = vim.fs.joinpath(plug_path, 'after') eq(true, vim.tbl_contains(rtp, plug_path)) eq(true, vim.tbl_contains(rtp, after_dir)) end validate(nil, { true, true, true, true, true, true, true, true }) n.clear() validate(false, {}) end) it('can use function `opts.load`', function() local validate = function() n.exec_lua(function() _G.load_log = {} local load = function(...) table.insert(_G.load_log, { ... }) end vim.pack.add({ repos_src.plugindirs, repos_src.basic }, { load = load }) end) -- Order of execution should be the same as supplied in `add()` local plugindirs_data = { spec = { src = repos_src.plugindirs, name = 'plugindirs' }, path = pack_get_plug_path('plugindirs'), } local basic_data = { spec = { src = repos_src.basic, name = 'basic' }, path = pack_get_plug_path('basic'), } -- - Only single table argument should be supplied to `load` local ref_log = { { plugindirs_data }, { basic_data } } eq(ref_log, n.exec_lua('return _G.load_log')) -- Should not add plugin to the session in any way eq(false, exec_lua('return pcall(require, "plugindirs")')) eq(false, exec_lua('return pcall(require, "basic")')) -- Should not source 'plugin/' eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) -- Plugins should still be marked as "active", since they were added plugindirs_data.spec.version = 'main' plugindirs_data.active = true basic_data.spec.version = 'main' basic_data.active = true eq({ plugindirs_data, basic_data }, n.exec_lua('return vim.pack.get()')) end -- Works on initial install validate() -- Works when loading already installed plugin n.clear() validate() end) it('generates help tags', function() exec_lua(function() vim.pack.add({ { src = repos_src.helptags, name = 'help tags' } }) end) local target_tags = fn.getcompletion('my-test', 'help') table.sort(target_tags) eq({ 'my-test-help', 'my-test-help-bad', 'my-test-help-sub-bad' }, target_tags) end) it('reports install/load errors after loading all input', function() t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'") local validate = function(err_pat) local err = pcall_err(exec_lua, function() vim.pack.add({ { src = repos_src.basic, version = 'wrong-version' }, -- Error during initial checkout { src = repos_src.semver, version = vim.version.range('>=2.0.0') }, -- Missing version { src = repos_src.plugindirs, version = 'main' }, { src = repos_src.pluginerr, version = 'main' }, -- Error during 'plugin/' source }) end) matches(err_pat, err) -- Should have processed non-errored 'plugin/' and add to 'rtp' eq('plugindirs main', exec_lua('return require("plugindirs")')) eq(true, exec_lua('return vim.g._plugin')) -- Should add plugin to 'rtp' even if 'plugin/' has error eq('pluginerr main', exec_lua('return require("pluginerr")')) end -- During initial install local err_pat_parts = { 'vim%.pack', '`basic`:\n', -- Should report available branches and tags if revision is absent '`wrong%-version`', 'Available:\nTags: some%-tag\nBranches: feat%-branch, main', -- Should report available branches and versions if no constraint match '`semver`', 'Available:\nVersions: v1%.0%.0, v0%.4, 0%.3%.1, v0%.3%.0.*\nBranches: main\n', '`pluginerr`:\n', 'Wow, an error', } validate(table.concat(err_pat_parts, '.*')) -- During loading already installed plugin. n.clear() -- NOTE: There is no error for wrong `version`, because there is no check -- for already installed plugins. Might change in the future. validate('vim%.pack.*`pluginerr`:\n.*Wow, an error') end) it('normalizes each spec', function() exec_lua(function() vim.pack.add({ repos_src.basic, -- String should be inferred as `{ src = ... }` { src = repos_src.defbranch }, -- Default `version` is remote's default branch { src = repos_src['gitsuffix.git'] }, -- Default `name` comes from `src` repo name { src = repos_src.plugindirs, name = 'plugin/dirs' }, -- Ensure proper directory name }) end) eq('basic main', exec_lua('return require("basic")')) eq('defbranch dev', exec_lua('return require("defbranch")')) eq('gitsuffix main', exec_lua('return require("gitsuffix")')) eq(true, exec_lua('return vim.g._plugin')) eq(true, pack_exists('gitsuffix')) eq(true, pack_exists('dirs')) end) it('handles problematic names', function() exec_lua(function() vim.pack.add({ { src = repos_src.basic, name = 'bad % name' } }) end) eq('basic main', exec_lua('return require("basic")')) end) it('validates input', function() local validate = function(err_pat, input) local add_input = function() vim.pack.add(input) end matches(err_pat, pcall_err(exec_lua, add_input)) end -- Separate spec entries validate('list', repos_src.basic) validate('spec:.*table', { 1 }) validate('spec%.src:.*string', { { src = 1 } }) validate('spec%.src:.*non%-empty string', { { src = '' } }) validate('spec%.name:.*string', { { src = repos_src.basic, name = 1 } }) validate('spec%.name:.*non%-empty string', { { src = repos_src.basic, name = '' } }) validate( 'spec%.version:.*string or vim%.VersionRange', { { src = repos_src.basic, version = 1 } } ) -- Conflicts in input array local version_conflict = { { src = repos_src.basic, version = 'feat-branch' }, { src = repos_src.basic, version = 'main' }, } validate('Conflicting `version` for `basic`.*feat%-branch.*main', version_conflict) local src_conflict = { { src = repos_src.basic, name = 'my-plugin' }, { src = repos_src.semver, name = 'my-plugin' }, } validate('Conflicting `src` for `my%-plugin`.*basic.*semver', src_conflict) end) end) describe('update()', function() -- Lua source code for the tested plugin named "fetch" local fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua') -- Table with hashes used to test confirmation buffer and log content local hashes --- @type table before_each(function() -- Create a dedicated clean repo for which "push changes" will be mocked init_test_repo('fetch') repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch init"') git_add_commit('Initial commit for "fetch"', 'fetch') repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch main"') git_add_commit('Commit from `main` to be removed', 'fetch') hashes = { fetch_head = git_get_hash('HEAD', 'fetch') } -- Install initial versions of tested plugins exec_lua(function() vim.pack.add({ repos_src.fetch, { src = repos_src.semver, version = 'v0.3.0' }, repos_src.defbranch, }) end) n.clear() -- Mock remote repo update -- - Force push repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new"') git_cmd({ 'add', '*' }, 'fetch') git_cmd({ 'commit', '--amend', '-m', 'Commit to be added 1' }, 'fetch') -- - Presence of a tag (should be shown in changelog) git_cmd({ 'tag', 'dev-tag' }, 'fetch') repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"') git_add_commit('Commit to be added 2', 'fetch') end) after_each(function() pcall(vim.fs.rm, repo_get_path('fetch'), { force = true, recursive = true }) local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') pcall(vim.fs.rm, log_path, { force = true }) end) describe('confirmation buffer', function() it('works', function() exec_lua(function() vim.pack.add({ repos_src.fetch, { src = repos_src.semver, version = 'v0.3.0' }, { src = repos_src.defbranch, version = 'does-not-exist' }, }) end) eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file)) exec_lua(function() -- Enable highlighting of special filetype vim.cmd('filetype plugin on') vim.pack.update() end) -- Buffer should be special and shown in a separate tabpage eq(2, #api.nvim_list_tabpages()) eq(2, fn.tabpagenr()) eq(api.nvim_get_option_value('filetype', {}), 'nvim-pack') eq(api.nvim_get_option_value('modifiable', {}), false) eq(api.nvim_get_option_value('buftype', {}), 'acwrite') local confirm_bufnr = api.nvim_get_current_buf() local confirm_winnr = api.nvim_get_current_win() local confirm_tabpage = api.nvim_get_current_tabpage() eq(api.nvim_buf_get_name(0), 'nvim-pack://' .. confirm_bufnr .. '/confirm-update') -- Adjust lines for a more robust screenshot testing local fetch_src = repos_src.fetch local fetch_path = pack_get_plug_path('fetch') local semver_src = repos_src.semver local semver_path = pack_get_plug_path('semver') exec_lua(function() -- Replace matches in line to preserve extmark highlighting local function replace_in_line(i, pattern, repl) local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] local from, to = line:find(pattern) while from and to do vim.api.nvim_buf_set_text(0, i - 1, from - 1, i - 1, to, { repl }) line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] from, to = line:find(pattern) end end vim.bo.modifiable = true local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) local pack_runtime = vim.fs.joinpath(vim.env.VIMRUNTIME, 'lua', 'vim', 'pack.lua') -- NOTE: replace path to `vim.pack` in error traceback accounting for -- possibly different slashes on Windows local pack_runtime_pattern = vim.pesc(pack_runtime):gsub('/', '[\\/]') .. ':%d+' for i = 1, #lines do replace_in_line(i, pack_runtime_pattern, 'VIM_PACK_RUNTIME') replace_in_line(i, vim.pesc(fetch_path), 'FETCH_PATH') replace_in_line(i, vim.pesc(fetch_src), 'FETCH_SRC') replace_in_line(i, vim.pesc(semver_path), 'SEMVER_PATH') replace_in_line(i, vim.pesc(semver_src), 'SEMVER_SRC') end vim.bo.modified = false vim.bo.modifiable = false end) -- Use screenshot to test highlighting, otherwise prefer text matching. -- This requires computing target hashes on each test run because they -- change due to source repos being cleanly created on each file test. local screen screen = Screen.new(85, 35) hashes.fetch_new = git_get_hash('HEAD', 'fetch') hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch') hashes.semver_head = git_get_hash('v0.3.0', 'semver') local tab_name = 'n' .. (t.is_os('win') and ':' or '') .. '//2/confirm-update' local screen_lines = { ('{24: [No Name] }{5: %s }{2:%s }{24:X}|'):format( tab_name, t.is_os('win') and '' or ' ' ), '{19:^# Error ────────────────────────────────────────────────────────────────────────} |', ' |', '{19:## defbranch} |', ' |', ' VIM_PACK_RUNTIME: `does-not-exist` is not a branch/tag/commit. Available: |', ' Tags: |', ' Branches: dev, main |', ' |', '{101:# Update ───────────────────────────────────────────────────────────────────────} |', ' |', '{101:## fetch} |', 'Path: {103:FETCH_PATH} |', 'Source: {103:FETCH_SRC} |', ('State before: {103:%s} |'):format( hashes.fetch_head ), ('State after: {103:%s} {102:(main)} |'):format( hashes.fetch_new ), ' |', 'Pending updates: |', ('{104:< %s │ Commit from `main` to be removed} |'):format( hashes.fetch_head ), ('{105:> %s │ Commit to be added 2} |'):format( hashes.fetch_new ), ('{105:> %s │ Commit to be added 1 (tag: dev-tag)} |'):format( hashes.fetch_new_prev ), ' |', '{102:# Same ─────────────────────────────────────────────────────────────────────────} |', ' |', '{102:## semver} |', 'Path: {103:SEMVER_PATH} |', 'Source: {103:SEMVER_SRC} |', ('State: {103:%s} {102:(v0.3.0)} |'):format( hashes.semver_head ), ' |', 'Available newer versions: |', '• {102:v1.0.0} |', '• {102:v0.4} |', '• {102:0.3.1} |', '{1:~ }|', ' |', } screen:add_extra_attr_ids({ [101] = { foreground = Screen.colors.Orange }, [102] = { foreground = Screen.colors.LightGray }, [103] = { foreground = Screen.colors.LightBlue }, [104] = { foreground = Screen.colors.NvimDarkRed }, [105] = { foreground = Screen.colors.NvimDarkGreen }, }) -- NOTE: Non LuaJIT reports errors differently due to 'coxpcall' if is_jit() then screen:expect(table.concat(screen_lines, '\n')) end -- `:write` should confirm n.exec('write') -- - Apply changes immediately eq({ 'return "fetch new 2"' }, fn.readfile(fetch_lua_file)) -- - Clean up buffer+window+tabpage eq(false, api.nvim_buf_is_valid(confirm_bufnr)) eq(false, api.nvim_win_is_valid(confirm_winnr)) eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) -- - Write to log file local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') local log_lines = fn.readfile(log_path) matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_lines[1]) local ref_log_lines = { '# Update ───────────────────────────────────────────────────────────────────────', '', '## fetch', 'Path: ' .. fetch_path, 'Source: ' .. fetch_src, 'State before: ' .. hashes.fetch_head, 'State after: ' .. hashes.fetch_new .. ' (main)', '', 'Pending updates:', '< ' .. hashes.fetch_head .. ' │ Commit from `main` to be removed', '> ' .. hashes.fetch_new .. ' │ Commit to be added 2', '> ' .. hashes.fetch_new_prev .. ' │ Commit to be added 1 (tag: dev-tag)', '', } eq(ref_log_lines, vim.list_slice(log_lines, 2)) end) it('can be dismissed with `:quit`', function() exec_lua(function() vim.pack.add({ repos_src.fetch }) vim.pack.update({ 'fetch' }) end) eq('nvim-pack', api.nvim_get_option_value('filetype', {})) -- Should not apply updates n.exec('quit') eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file)) end) it('closes full tabpage', function() exec_lua(function() vim.pack.add({ repos_src.fetch }) vim.pack.update() end) -- Confirm with `:write` local confirm_tabpage = api.nvim_get_current_tabpage() n.exec('-tab split other-tab') local other_tabpage = api.nvim_get_current_tabpage() n.exec('tabnext') n.exec('write') eq(true, api.nvim_tabpage_is_valid(other_tabpage)) eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) -- Not confirm with `:quit` n.exec('tab split other-tab-2') local other_tabpage_2 = api.nvim_get_current_tabpage() exec_lua(function() vim.pack.update() end) confirm_tabpage = api.nvim_get_current_tabpage() -- - Temporary split window in tabpage should not matter n.exec('vsplit other-buf') n.exec('wincmd w') n.exec('tabclose ' .. api.nvim_tabpage_get_number(other_tabpage_2)) eq(confirm_tabpage, api.nvim_get_current_tabpage()) n.exec('quit') eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) end) it('has in-process LSP features', function() t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'") exec_lua(function() vim.pack.add({ repos_src.fetch, { src = repos_src.semver, version = 'v0.3.0' }, { src = repos_src.defbranch, version = 'does-not-exist' }, }) vim.pack.update() end) eq(1, exec_lua('return #vim.lsp.get_clients({ bufnr = 0 })')) -- textDocument/documentSymbol exec_lua('vim.lsp.buf.document_symbol()') local loclist = vim.tbl_map(function(x) --- @param x table return { lnum = x.lnum, --- @type integer col = x.col, --- @type integer end_lnum = x.end_lnum, --- @type integer end_col = x.end_col, --- @type integer text = x.text, --- @type string } end, fn.getloclist(0)) local ref_loclist = { { lnum = 1, col = 1, end_lnum = 9, end_col = 1, text = '[Namespace] Error' }, { lnum = 3, col = 1, end_lnum = 9, end_col = 1, text = '[Module] defbranch' }, { lnum = 9, col = 1, end_lnum = 22, end_col = 1, text = '[Namespace] Update' }, { lnum = 11, col = 1, end_lnum = 22, end_col = 1, text = '[Module] fetch' }, { lnum = 22, col = 1, end_lnum = 32, end_col = 1, text = '[Namespace] Same' }, { lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver' }, } eq(ref_loclist, loclist) n.exec('lclose') -- textDocument/hover local confirm_winnr = api.nvim_get_current_win() local validate_hover = function(pos, commit_msg) api.nvim_win_set_cursor(0, pos) exec_lua(function() vim.lsp.buf.hover() -- Default hover is async shown in floating window vim.wait(1000, function() return #vim.api.nvim_tabpage_list_wins(0) > 1 end) end) local all_wins = api.nvim_tabpage_list_wins(0) eq(2, #all_wins) local float_winnr = all_wins[1] == confirm_winnr and all_wins[2] or all_wins[1] eq(true, api.nvim_win_get_config(float_winnr).relative ~= '') local float_buf = api.nvim_win_get_buf(float_winnr) local text = table.concat(api.nvim_buf_get_lines(float_buf, 0, -1, false), '\n') local ref_pattern = 'Marvim \nDate:.*' .. vim.pesc(commit_msg) matches(ref_pattern, text) end validate_hover({ 14, 0 }, 'Commit from `main` to be removed') validate_hover({ 15, 0 }, 'Commit to be added 2') validate_hover({ 18, 0 }, 'Commit from `main` to be removed') validate_hover({ 19, 0 }, 'Commit to be added 2') validate_hover({ 20, 0 }, 'Commit to be added 1') validate_hover({ 27, 0 }, 'Add version v0.3.0') validate_hover({ 30, 0 }, 'Add version v1.0.0') validate_hover({ 31, 0 }, 'Add version v0.4') validate_hover({ 32, 0 }, 'Add version 0.3.1') end) it('suggests newer versions when on non-tagged commit', function() local commit = git_get_hash('0.3.1~', 'semver') exec_lua(function() -- Make fresh install for cleaner test vim.pack.del({ 'semver' }) vim.pack.add({ { src = repos_src.semver, version = commit } }) vim.pack.update({ 'semver' }) end) -- Should correctly infer that 0.3.0 is the latest version and suggest -- versions greater than that local confirm_text = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), '\n') matches('Available newer versions:\n• v1%.0%.0\n• v0%.4\n• 0%.3%.1$', confirm_text) end) end) it('works with not active plugins', function() exec_lua(function() -- No plugins are added, but they are installed in `before_each()` vim.pack.update({ 'fetch' }) end) eq({ 'return "fetch main"' }, fn.readfile(fetch_lua_file)) n.exec('write') eq({ 'return "fetch new 2"' }, fn.readfile(fetch_lua_file)) end) it('can force update', function() exec_lua(function() vim.pack.add({ repos_src.fetch }) vim.pack.update({ 'fetch' }, { force = true }) end) -- Apply changes immediately local fetch_src = repos_src.fetch local fetch_path = pack_get_plug_path('fetch') eq({ 'return "fetch new 2"' }, fn.readfile(fetch_lua_file)) -- No special buffer/window/tabpage eq(1, #api.nvim_list_tabpages()) eq(1, #api.nvim_list_wins()) eq('', api.nvim_get_option_value('filetype', {})) -- Write to log file hashes.fetch_new = git_get_hash('HEAD', 'fetch') hashes.fetch_new_prev = git_get_hash('HEAD~', 'fetch') local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') local log_lines = fn.readfile(log_path) matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_lines[1]) local ref_log_lines = { '# Update ───────────────────────────────────────────────────────────────────────', '', '## fetch', 'Path: ' .. fetch_path, 'Source: ' .. fetch_src, 'State before: ' .. hashes.fetch_head, 'State after: ' .. hashes.fetch_new .. ' (main)', '', 'Pending updates:', '< ' .. hashes.fetch_head .. ' │ Commit from `main` to be removed', '> ' .. hashes.fetch_new .. ' │ Commit to be added 2', '> ' .. hashes.fetch_new_prev .. ' │ Commit to be added 1 (tag: dev-tag)', '', } eq(ref_log_lines, vim.list_slice(log_lines, 2)) end) it('shows progress report', function() exec_lua(function() vim.pack.add({ repos_src.fetch, repos_src.defbranch }) vim.pack.update() end) -- During initial download validate_progress_report('Downloading updates', { 'fetch', 'defbranch' }) n.exec('messages clear') -- During application (only for plugins that have updates) n.exec('write') validate_progress_report('Applying updates', { 'fetch' }) -- During force update n.clear() repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"') git_add_commit('Commit to be added 3', 'fetch') exec_lua(function() vim.pack.add({ repos_src.fetch, repos_src.defbranch }) vim.pack.update(nil, { force = true }) end) validate_progress_report('Updating', { 'fetch', 'defbranch' }) end) it('triggers relevant events', function() watch_events({ 'PackChangedPre', 'PackChanged' }) exec_lua(function() vim.pack.add({ repos_src.fetch, repos_src.defbranch }) _G.event_log = {} vim.pack.update() end) eq({}, exec_lua('return _G.event_log')) -- Should trigger relevant events only for actually updated plugins n.exec('write') local log = exec_lua('return _G.event_log') eq(1, find_in_log(log, 'PackChangedPre', 'update', 'fetch', 'main')) eq(2, find_in_log(log, 'PackChanged', 'update', 'fetch', 'main')) eq(2, #log) end) it('stashes before applying changes', function() fn.writefile({ 'A text that will be stashed' }, fetch_lua_file) exec_lua(function() vim.pack.add({ repos_src.fetch }) vim.pack.update() vim.cmd('write') end) local fetch_path = pack_get_plug_path('fetch') local stash_list = system_sync({ 'git', 'stash', 'list' }, { cwd = fetch_path }).stdout or '' matches('vim%.pack: %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d Stash before checkout', stash_list) -- Update should still be applied eq({ 'return "fetch new 2"' }, fn.readfile(fetch_lua_file)) end) it('validates input', function() local validate = function(err_pat, input) local update_input = function() vim.pack.update(input) end matches(err_pat, pcall_err(exec_lua, update_input)) end validate('list', 1) -- Should first check if every plugin name represents installed plugin -- If not - stop early before any update exec_lua(function() vim.pack.add({ repos_src.basic }) end) validate('The following plugins are not installed: aaa, ccc', { 'aaa', 'basic', 'ccc' }) -- Empty list is allowed with warning n.exec('messages clear') exec_lua(function() vim.pack.update({}) end) eq('vim.pack: Nothing to update', n.exec_capture('messages')) end) end) describe('get()', function() local basic_spec = { name = 'basic', src = repos_src.basic, version = 'main' } local basic_path = pack_get_plug_path('basic') local defbranch_spec = { name = 'defbranch', src = repos_src.defbranch, version = 'dev' } local defbranch_path = pack_get_plug_path('defbranch') it('returns list of available plugins', function() -- Should work just after installation exec_lua(function() vim.pack.add({ repos_src.defbranch, repos_src.basic }) end) eq({ -- Should preserve order in which plugins were `vim.pack.add()`ed { active = true, path = defbranch_path, spec = defbranch_spec }, { active = true, path = basic_path, spec = basic_spec }, }, exec_lua('return vim.pack.get()')) -- Should also list non-active plugins n.clear() exec_lua(function() vim.pack.add({ repos_src.basic }) end) eq({ -- Should first list active, then non-active { active = true, path = basic_path, spec = basic_spec }, { active = false, path = defbranch_path, spec = defbranch_spec }, }, exec_lua('return vim.pack.get()')) end) it('respects data field', function() eq( true, exec_lua(function() vim.pack.add { { src = repos_src.basic, data = { test = 'value' } }, } for _, p in ipairs(vim.pack.get()) do if p.spec.name == 'basic' and p.spec.data.test == 'value' then return true end end return false end) ) end) it('works with `del()`', function() exec_lua(function() vim.pack.add({ repos_src.defbranch, repos_src.basic }) end) exec_lua(function() _G.get_log = {} vim.api.nvim_create_autocmd({ 'PackChangedPre', 'PackChanged' }, { callback = function() table.insert(_G.get_log, vim.pack.get()) end, }) end) -- Should not include removed plugins immediately after they are removed, -- while still returning list without holes exec_lua('vim.pack.del({ "defbranch" })') eq({ { { active = true, path = defbranch_path, spec = defbranch_spec }, { active = true, path = basic_path, spec = basic_spec }, }, { { active = true, path = basic_path, spec = basic_spec }, }, }, exec_lua('return _G.get_log')) end) end) describe('del()', function() it('works', function() exec_lua(function() vim.pack.add({ repos_src.plugindirs, { src = repos_src.basic, version = 'feat-branch' } }) end) eq(true, pack_exists('basic')) eq(true, pack_exists('plugindirs')) watch_events({ 'PackChangedPre', 'PackChanged' }) n.exec('messages clear') exec_lua(function() vim.pack.del({ 'basic', 'plugindirs' }) end) eq(false, pack_exists('basic')) eq(false, pack_exists('plugindirs')) eq( "vim.pack: Removed plugin 'plugindirs'\nvim.pack: Removed plugin 'basic'", n.exec_capture('messages') ) -- Should trigger relevant events in order as specified in `vim.pack.add()` local log = exec_lua('return _G.event_log') eq(1, find_in_log(log, 'PackChangedPre', 'delete', 'plugindirs', 'main')) eq(2, find_in_log(log, 'PackChanged', 'delete', 'plugindirs', 'main')) eq(3, find_in_log(log, 'PackChangedPre', 'delete', 'basic', 'feat-branch')) eq(4, find_in_log(log, 'PackChanged', 'delete', 'basic', 'feat-branch')) eq(4, #log) end) it('validates input', function() local validate = function(err_pat, input) local del_input = function() vim.pack.del(input) end matches(err_pat, pcall_err(exec_lua, del_input)) end validate('list', nil) -- Should first check if every plugin name represents installed plugin -- If not - stop early before any delete exec_lua(function() vim.pack.add({ repos_src.basic }) end) validate('The following plugins are not installed: aaa, ccc', { 'aaa', 'basic', 'ccc' }) eq(true, pack_exists('basic')) -- Empty list is allowed with warning n.exec('messages clear') exec_lua(function() vim.pack.del({}) end) eq('vim.pack: Nothing to remove', n.exec_capture('messages')) end) end) end)