commit 6e2720090130f65e244a42a595a274dd5bad3d37
parent 52a596b0aaece2fc9744ea20b24adc26c213be34
Author: Evgeni Chasnovski <evgeni.chasnovski@gmail.com>
Date: Sat, 2 Aug 2025 14:58:27 +0300
test(pack): add `vim.pack` tests
Diffstat:
2 files changed, 1136 insertions(+), 39 deletions(-)
diff --git a/runtime/ftplugin/nvim-pack.lua b/runtime/ftplugin/nvim-pack.lua
@@ -5,6 +5,8 @@ local priority = 100
local hi_range = function(lnum, start_col, end_col, hl, pr)
--- @type vim.api.keyset.set_extmark
local opts = { end_row = lnum - 1, end_col = end_col, hl_group = hl, priority = pr or priority }
+ -- Set expanding gravity for easier testing. Should not make big difference.
+ opts.right_gravity, opts.end_right_gravity = false, true
vim.api.nvim_buf_set_extmark(0, ns, lnum - 1, start_col, opts)
end
@@ -30,8 +32,10 @@ for i, l in ipairs(lines) do
hi_range(i, cur_info:len(), end_col, 'DiagnosticInfo')
-- Plugin state after update
- local col = l:match('() %b()$') or l:len()
- hi_range(i, col, l:len(), 'DiagnosticHint')
+ local col = l:match('() %b()$')
+ if col then
+ hi_range(i, col, l:len(), 'DiagnosticHint')
+ end
elseif l:match('^> ') then
-- Added change with possibly "breaking message"
hi_range(i, 0, l:len(), 'Added')
diff --git a/test/functional/plugin/pack_spec.lua b/test/functional/plugin/pack_spec.lua
@@ -1,73 +1,1166 @@
+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<string,string>
+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<string,function>
+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.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.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<string,boolean>
+ 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()
- pending('works', function()
- -- TODO
+ 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('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('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)
+
+ 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)
- pending('reports errors after loading', function()
- -- TODO
- -- Should handle (not let it terminate the function) and report errors from pack_add()
+ 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)
- pending('respects after/', function()
- -- TODO
- -- Should source 'after/plugin/' directory (even nested files) after
- -- all 'plugin/' files are sourced in all plugins from input.
- --
- -- Should add 'after/' directory (if present) to 'runtimepath'
+ 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)
- pending('normalizes each spec', function()
- -- TODO
+ 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'))
- -- TODO: Should properly infer `name` from `src` (as its basename
- -- minus '.git' suffix) but allow '.git' suffix in explicit `name`
+ eq(true, pack_exists('gitsuffix'))
+ eq(true, pack_exists('dirs'))
end)
- pending('normalizes spec array', function()
- -- TODO
- -- Should silently ignore full duplicates (same `src`+`version`)
- -- and error on conflicts.
+ 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)
- pending('installs', function()
- -- TODO
+ 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
- -- TODO: Should block code flow until all plugins are available on disk
- -- and `:packadd` all of them (even just now installed) as a result.
+ -- 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()
- pending('works', function()
- -- TODO
+ -- 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<string,string>
+
+ 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 })'))
- -- TODO: Should work with both added and not added plugins
+ -- 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 <marvim@neovim%.io>\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)
- pending('suggests newer tags if there are no updates', function()
- -- TODO
+ 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
- -- TODO: Should not suggest tags that point to the current state.
- -- Even if there is one/several and located at start/middle/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()
- pending('works', function()
- -- TODO
+ 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)
- pending('works after `del()`', function()
- -- TODO: Should not include removed plugins and still return list
+ it('works with `del()`', function()
+ exec_lua(function()
+ vim.pack.add({ repos_src.defbranch, repos_src.basic })
+ end)
- -- TODO: Should return corrent list inside `PackChanged` "delete" event
+ 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()
- pending('works', function()
- -- TODO
+ 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)