pack_spec.lua (85630B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 local Screen = require('test.functional.ui.screen') 4 local skip_integ = os.getenv('NVIM_TEST_INTEG') ~= '1' 5 6 local api = n.api 7 local fn = n.fn 8 9 local eq = t.eq 10 local matches = t.matches 11 local pcall_err = t.pcall_err 12 local exec_lua = n.exec_lua 13 14 -- Helpers ==================================================================== 15 -- Installed plugins ---------------------------------------------------------- 16 17 local function pack_get_dir() 18 return vim.fs.joinpath(fn.stdpath('data'), 'site', 'pack', 'core', 'opt') 19 end 20 21 local function pack_get_plug_path(plug_name) 22 return vim.fs.joinpath(pack_get_dir(), plug_name) 23 end 24 25 local function pack_exists(plug_name) 26 local path = vim.fs.joinpath(pack_get_dir(), plug_name) 27 return vim.uv.fs_stat(path) ~= nil 28 end 29 30 --- Assert content from the main Lua file inside installed plugin. 31 --- Used as a proxy for checking plugin state on disk. 32 local function pack_assert_content(plug_name, content) 33 local full_path = vim.fs.joinpath(pack_get_plug_path(plug_name), 'lua', plug_name .. '.lua') 34 eq(content, fn.readblob(full_path)) 35 end 36 37 -- Test repos (to be installed) ----------------------------------------------- 38 39 local repos_dir = vim.fs.abspath('test/functional/lua/pack-test-repos') 40 41 --- Map from repo name to its proper `src` used in plugin spec 42 --- @type table<string,string> 43 local repos_src = {} 44 45 local function repo_get_path(repo_name) 46 vim.validate('repo_name', repo_name, 'string') 47 return vim.fs.joinpath(repos_dir, repo_name) 48 end 49 50 local function repo_write_file(repo_name, rel_path, text, no_dedent, append) 51 local path = vim.fs.joinpath(repo_get_path(repo_name), rel_path) 52 fn.mkdir(vim.fs.dirname(path), 'p') 53 t.write_file(path, text, no_dedent, append) 54 end 55 56 --- @return vim.SystemCompleted 57 local function system_sync(cmd, opts) 58 return exec_lua(function() 59 local obj = vim.system(cmd, opts) 60 61 if opts and opts.timeout then 62 -- Minor delay before calling wait() so the timeout uv timer can have a headstart over the 63 -- internal call to vim.wait() in wait(). 64 vim.wait(10) 65 end 66 67 local res = obj:wait() 68 69 -- Check the process is no longer running 70 assert(not vim.api.nvim_get_proc(obj.pid), 'process still exists') 71 72 return res 73 end) 74 end 75 76 local function git_cmd(cmd, repo_name) 77 local git_cmd_prefix = { 78 'git', 79 '-c', 80 'gc.auto=0', 81 '-c', 82 'user.name=Marvim', 83 '-c', 84 'user.email=marvim@neovim.io', 85 '-c', 86 'init.defaultBranch=main', 87 } 88 89 cmd = vim.list_extend(git_cmd_prefix, cmd) 90 local cwd = repo_get_path(repo_name) 91 local sys_opts = { cwd = cwd, text = true, clear_env = true } 92 local out = system_sync(cmd, sys_opts) 93 if out.code ~= 0 then 94 error(out.stderr) 95 end 96 return (out.stdout:gsub('\n+$', '')) 97 end 98 99 local function init_test_repo(repo_name) 100 local path = repo_get_path(repo_name) 101 fn.mkdir(path, 'p') 102 repos_src[repo_name] = 'file://' .. path 103 104 git_cmd({ 'init' }, repo_name) 105 end 106 107 local function git_add_commit(msg, repo_name) 108 git_cmd({ 'add', '*' }, repo_name) 109 git_cmd({ 'commit', '-m', msg }, repo_name) 110 end 111 112 local function git_get_hash(rev, repo_name) 113 return git_cmd({ 'rev-list', '-1', rev }, repo_name) 114 end 115 116 local function git_get_short_hash(rev, repo_name) 117 return git_cmd({ 'rev-list', '-1', '--abbrev-commit', rev }, repo_name) 118 end 119 120 -- Common test repos ---------------------------------------------------------- 121 --- @type table<string,function> 122 local repos_setup = {} 123 124 function repos_setup.basic() 125 init_test_repo('basic') 126 127 repo_write_file('basic', 'lua/basic.lua', 'return "basic init"') 128 git_add_commit('Initial commit for "basic"', 'basic') 129 repo_write_file('basic', 'lua/basic.lua', 'return "basic main"') 130 git_add_commit('Commit in `main` but not in `feat-branch`', 'basic') 131 132 git_cmd({ 'checkout', 'main~' }, 'basic') 133 git_cmd({ 'checkout', '-b', 'feat-branch' }, 'basic') 134 135 repo_write_file('basic', 'lua/basic.lua', 'return "basic some-tag"') 136 git_add_commit('Add commit for some tag', 'basic') 137 git_cmd({ 'tag', 'some-tag' }, 'basic') 138 139 repo_write_file('basic', 'lua/basic.lua', 'return "basic feat-branch"') 140 git_add_commit('Add important feature', 'basic') 141 142 -- Make sure that `main` is the default remote branch 143 git_cmd({ 'checkout', 'main' }, 'basic') 144 end 145 146 function repos_setup.plugindirs() 147 init_test_repo('plugindirs') 148 149 -- Add semver tag 150 repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs v0.0.1"') 151 git_add_commit('Add version v0.0.1', 'plugindirs') 152 git_cmd({ 'tag', 'v0.0.1' }, 'plugindirs') 153 154 -- Add various 'plugin/' files 155 repo_write_file('plugindirs', 'lua/plugindirs.lua', 'return "plugindirs main"') 156 repo_write_file('plugindirs', 'plugin/dirs.lua', 'vim.g._plugin = true') 157 repo_write_file('plugindirs', 'plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "p"') 158 repo_write_file('plugindirs', 'plugin/dirs.vim', 'let g:_plugin_vim=v:true') 159 repo_write_file('plugindirs', 'plugin/sub/dirs.lua', 'vim.g._plugin_sub = true') 160 repo_write_file('plugindirs', 'plugin/bad % name.lua', 'vim.g._plugin_bad = true') 161 repo_write_file('plugindirs', 'after/plugin/dirs.lua', 'vim.g._after_plugin = true') 162 repo_write_file('plugindirs', 'after/plugin/dirs_log.lua', '_G.DL = _G.DL or {}; DL[#DL+1] = "a"') 163 repo_write_file('plugindirs', 'after/plugin/dirs.vim', 'let g:_after_plugin_vim=v:true') 164 repo_write_file('plugindirs', 'after/plugin/sub/dirs.lua', 'vim.g._after_plugin_sub = true') 165 repo_write_file('plugindirs', 'after/plugin/bad % name.lua', 'vim.g._after_plugin_bad = true') 166 git_add_commit('Initial commit for "plugindirs"', 'plugindirs') 167 end 168 169 function repos_setup.helptags() 170 init_test_repo('helptags') 171 repo_write_file('helptags', 'lua/helptags.lua', 'return "helptags main"') 172 repo_write_file('helptags', 'doc/my-test-help.txt', '*my-test-help*') 173 repo_write_file('helptags', 'doc/bad % name.txt', '*my-test-help-bad*') 174 repo_write_file('helptags', 'doc/bad % dir/file.txt', '*my-test-help-sub-bad*') 175 git_add_commit('Initial commit for "helptags"', 'helptags') 176 end 177 178 function repos_setup.pluginerr() 179 init_test_repo('pluginerr') 180 181 repo_write_file('pluginerr', 'lua/pluginerr.lua', 'return "pluginerr main"') 182 repo_write_file('pluginerr', 'plugin/err.lua', 'error("Wow, an error")') 183 git_add_commit('Initial commit for "pluginerr"', 'pluginerr') 184 end 185 186 function repos_setup.defbranch() 187 init_test_repo('defbranch') 188 189 repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch main"') 190 git_add_commit('Initial commit for "defbranch"', 'defbranch') 191 192 -- Make `dev` the default remote branch 193 git_cmd({ 'checkout', '-b', 'dev' }, 'defbranch') 194 195 repo_write_file('defbranch', 'lua/defbranch.lua', 'return "defbranch dev"') 196 git_add_commit('Add to new default branch', 'defbranch') 197 end 198 199 function repos_setup.gitsuffix() 200 init_test_repo('gitsuffix.git') 201 202 repo_write_file('gitsuffix.git', 'lua/gitsuffix.lua', 'return "gitsuffix main"') 203 git_add_commit('Initial commit for "gitsuffix"', 'gitsuffix.git') 204 end 205 206 function repos_setup.semver() 207 init_test_repo('semver') 208 209 local function add_tag(name) 210 repo_write_file('semver', 'lua/semver.lua', 'return "semver ' .. name .. '"') 211 git_add_commit('Add version ' .. name, 'semver') 212 git_cmd({ 'tag', name }, 'semver') 213 end 214 215 add_tag('v0.0.1') 216 add_tag('v0.0.2') 217 add_tag('v0.1.0') 218 add_tag('v0.1.1') 219 add_tag('v0.2.0-dev') 220 add_tag('v0.2.0') 221 add_tag('v0.3.0') 222 repo_write_file('semver', 'lua/semver.lua', 'return "semver middle-commit') 223 git_add_commit('Add middle commit', 'semver') 224 add_tag('0.3.1') 225 add_tag('v0.4') 226 add_tag('non-semver') 227 add_tag('v0.2.1') -- Intentionally add version not in order 228 add_tag('v1.0.0') 229 end 230 231 function repos_setup.with_subs() 232 -- To-be-submodule repo 233 init_test_repo('sub') 234 235 repo_write_file('sub', 'sub.lua', 'return "sub init"') 236 git_add_commit('Initial commit for "sub"', 'sub') 237 238 -- With-submodules repo with submodule recorded at its initial commit 239 init_test_repo('with_subs') 240 241 repo_write_file('with_subs', 'lua/with_subs.lua', 'return "with_subs init"') 242 local sub_src = 'file://' .. repo_get_path('sub') 243 git_cmd({ '-c', 'protocol.file.allow=always', 'submodule', 'add', sub_src, 'sub' }, 'with_subs') 244 git_add_commit('Initial commit for "with_subs"', 'with_subs') 245 git_cmd({ 'tag', 'init-commit' }, 'with_subs') 246 247 -- Advance both submodule and with-submodules repos by one commit 248 repo_write_file('sub', 'sub.lua', 'return "sub main"') 249 git_add_commit('Second commit for "sub"', 'sub') 250 251 repo_write_file('with_subs', 'lua/with_subs.lua', 'return "with_subs main"') 252 git_cmd( 253 { '-c', 'protocol.file.allow=always', 'submodule', 'update', '--remote', 'sub' }, 254 'with_subs' 255 ) 256 git_add_commit('Second commit for "with_subs"', 'with_subs') 257 end 258 259 -- Utility -------------------------------------------------------------------- 260 261 --- Execute `vim.pack.add()` inside `testnvim` instance 262 local function vim_pack_add(specs, opts) 263 exec_lua(function() 264 vim.pack.add(specs, opts) 265 end) 266 end 267 268 local function watch_events(event) 269 exec_lua(function() 270 _G.event_log = _G.event_log or {} --- @type table[] 271 vim.api.nvim_create_autocmd(event, { 272 callback = function(ev) 273 table.insert(_G.event_log, { event = ev.event, match = ev.match, data = ev.data }) 274 end, 275 }) 276 end) 277 end 278 279 --- @param log table[] 280 local function make_find_packchanged(log) 281 --- @param suffix string 282 return function(suffix, kind, repo_name, version, active) 283 local path = pack_get_plug_path(repo_name) 284 local spec = { name = repo_name, src = repos_src[repo_name], version = version } 285 local data = { active = active, kind = kind, path = path, spec = spec } 286 local entry = { event = 'PackChanged' .. suffix, match = vim.fs.abspath(path), data = data } 287 288 local res = 0 289 for i, tbl in ipairs(log) do 290 if vim.deep_equal(tbl, entry) then 291 res = i 292 break 293 end 294 end 295 eq(true, res > 0) 296 297 return res 298 end 299 end 300 301 local function track_nvim_echo() 302 exec_lua(function() 303 _G.echo_log = {} 304 local nvim_echo_orig = vim.api.nvim_echo 305 ---@diagnostic disable-next-line: duplicate-set-field 306 vim.api.nvim_echo = function(...) 307 table.insert(_G.echo_log, vim.deepcopy({ ... })) 308 return nvim_echo_orig(...) 309 end 310 end) 311 end 312 313 local function assert_progress_report(action, step_names) 314 -- NOTE: Assume that `nvim_echo` mocked log has only progress report messages 315 local echo_log = exec_lua('return _G.echo_log') ---@type table[] 316 local n_steps = #step_names 317 eq(n_steps + 2, #echo_log) 318 319 local progress = { kind = 'progress', title = 'vim.pack', status = 'running', percent = 0 } 320 local init_step = { { { ('%s (0/%d)'):format(action, n_steps) } }, true, progress } 321 eq(init_step, echo_log[1]) 322 323 local steps_seen = {} --- @type table<string,boolean> 324 for i = 1, n_steps do 325 local echo_args = echo_log[i + 1] 326 327 -- NOTE: There is no guaranteed order (as it is async), so check that some 328 -- expected step name is used in the message 329 local msg = ('%s (%d/%d)'):format(action, i, n_steps) 330 local pattern = '^' .. vim.pesc(msg) .. ' %- (%S+)$' 331 local step = echo_args[1][1][1]:match(pattern) ---@type string 332 eq(true, vim.tbl_contains(step_names, step)) 333 steps_seen[step] = true 334 335 -- Should not add intermediate progress report to history 336 eq(echo_args[2], false) 337 338 -- Should update a single message by its id (computed after first call) 339 progress.id = progress.id or echo_args[3].id ---@type integer 340 progress.percent = math.floor(100 * i / n_steps) 341 eq(echo_args[3], progress) 342 end 343 344 -- Should report all steps 345 eq(n_steps, vim.tbl_count(steps_seen)) 346 347 progress.percent, progress.status = 100, 'success' 348 local final_step = { { { ('%s (%d/%d)'):format(action, n_steps, n_steps) } }, true, progress } 349 eq(final_step, echo_log[n_steps + 2]) 350 end 351 352 local function mock_confirm(output_value) 353 exec_lua(function() 354 _G.confirm_log = _G.confirm_log or {} 355 356 ---@diagnostic disable-next-line: duplicate-set-field 357 vim.fn.confirm = function(...) 358 table.insert(_G.confirm_log, { ... }) 359 return output_value 360 end 361 end) 362 end 363 364 local function mock_git_file_transport() 365 -- HACK: mock `vim.system()` to have `git` commands be executed 366 -- with temporarily set 'protocol.file.allow=always' option. 367 -- Otherwise performing `git` operations with submodules from `vim.pack` 368 -- itself will fail with `fatal: transport 'file' not allowed`. 369 -- Directly adding `-c protocol.file.allow=always` to `git_cmd` in `vim.pack` 370 -- itself is too much and might be bad for security. 371 exec_lua(function() 372 local vim_system_orig = vim.system 373 vim.system = function(cmd, opts, on_exit) 374 if cmd[1] == 'git' then 375 table.insert(cmd, 2, '-c') 376 table.insert(cmd, 3, 'protocol.file.allow=always') 377 end 378 return vim_system_orig(cmd, opts, on_exit) 379 end 380 end) 381 end 382 383 local function is_jit() 384 return exec_lua('return package.loaded.jit ~= nil') 385 end 386 387 local function get_lock_path() 388 return vim.fs.joinpath(fn.stdpath('config'), 'nvim-pack-lock.json') 389 end 390 391 --- @return {plugins:table<string, {rev:string, src:string, version?:string}>} 392 local function get_lock_tbl() 393 return vim.json.decode(fn.readblob(get_lock_path())) 394 end 395 396 -- Tests ====================================================================== 397 398 describe('vim.pack', function() 399 setup(function() 400 n.clear() 401 for _, r_setup in pairs(repos_setup) do 402 r_setup() 403 end 404 end) 405 406 before_each(function() 407 n.clear() 408 end) 409 410 after_each(function() 411 vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) 412 vim.fs.rm(get_lock_path(), { force = true }) 413 local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') 414 pcall(vim.fs.rm, log_path, { force = true }) 415 end) 416 417 teardown(function() 418 vim.fs.rm(repos_dir, { force = true, recursive = true }) 419 end) 420 421 describe('add()', function() 422 it('installs only once', function() 423 vim_pack_add({ repos_src.basic }) 424 n.clear() 425 426 watch_events({ 'PackChanged' }) 427 vim_pack_add({ repos_src.basic }) 428 eq(exec_lua('return #_G.event_log'), 0) 429 430 -- Should not create redundant stash entry 431 local basic_path = pack_get_plug_path('basic') 432 local stash_list = system_sync({ 'git', 'stash', 'list' }, { cwd = basic_path }).stdout or '' 433 eq('', stash_list) 434 end) 435 436 it('passes `data` field through to `opts.load`', function() 437 local out = exec_lua(function() 438 local map = {} ---@type table<string,boolean> 439 local function load(p) 440 local name = p.spec.name ---@type string 441 map[name] = name == 'basic' and (p.spec.data.test == 'value') or (p.spec.data == 'value') 442 end 443 vim.pack.add({ 444 { src = repos_src.basic, data = { test = 'value' } }, 445 { src = repos_src.defbranch, data = 'value' }, 446 }, { load = load }) 447 return map 448 end) 449 eq({ basic = true, defbranch = true }, out) 450 end) 451 452 it('asks for installation confirmation', function() 453 -- Do not confirm installation to see what happens (should not error) 454 mock_confirm(2) 455 456 vim_pack_add({ repos_src.basic, { src = repos_src.defbranch, name = 'other-name' } }) 457 eq(false, pack_exists('basic')) 458 eq(false, pack_exists('defbranch')) 459 eq({ plugins = {} }, get_lock_tbl()) 460 461 local confirm_msg_lines = ([[ 462 These plugins will be installed: 463 464 basic from %s 465 other-name from %s]]):format(repos_src.basic, repos_src.defbranch) 466 local confirm_msg = vim.trim(vim.text.indent(0, confirm_msg_lines)) 467 local ref_log = { { confirm_msg .. '\n', 'Proceed? &Yes\n&No\n&Always', 1, 'Question' } } 468 eq(ref_log, exec_lua('return _G.confirm_log')) 469 470 -- Should remove lock data if not confirmed during lockfile sync 471 n.clear() 472 vim_pack_add({ repos_src.basic }) 473 eq(true, pack_exists('basic')) 474 eq('table', type(get_lock_tbl().plugins.basic)) 475 476 vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) 477 n.clear() 478 mock_confirm(2) 479 480 vim_pack_add({ repos_src.basic }) 481 eq(false, pack_exists('basic')) 482 eq({ plugins = {} }, get_lock_tbl()) 483 484 -- Should ask for confirm twice: during lockfile sync and inside 485 -- `vim.pack.add()` (i.e. not confirming during lockfile sync has 486 -- an immediate effect on whether a plugin is installed or not) 487 eq(2, exec_lua('return #_G.confirm_log')) 488 end) 489 490 it('respects `opts.confirm`', function() 491 mock_confirm(1) 492 vim_pack_add({ repos_src.basic }, { confirm = false }) 493 494 eq(0, exec_lua('return #_G.confirm_log')) 495 eq(true, pack_exists('basic')) 496 497 -- Should also respect `confirm` when installing during lockfile sync 498 vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) 499 eq('table', type(get_lock_tbl().plugins.basic)) 500 501 n.clear() 502 mock_confirm(1) 503 504 vim_pack_add({}, { confirm = false }) 505 eq(0, exec_lua('return #_G.confirm_log')) 506 eq(true, pack_exists('basic')) 507 end) 508 509 it('can always confirm in current session', function() 510 mock_confirm(3) 511 512 vim_pack_add({ repos_src.basic }) 513 eq(1, exec_lua('return #_G.confirm_log')) 514 eq('basic main', exec_lua('return require("basic")')) 515 516 vim_pack_add({ repos_src.defbranch }) 517 eq(1, exec_lua('return #_G.confirm_log')) 518 eq('defbranch dev', exec_lua('return require("defbranch")')) 519 520 -- Should still ask in next session 521 n.clear() 522 mock_confirm(3) 523 vim_pack_add({ repos_src.plugindirs }) 524 eq(1, exec_lua('return #_G.confirm_log')) 525 eq('plugindirs main', exec_lua('return require("plugindirs")')) 526 end) 527 528 it('creates lockfile', function() 529 local helptags_rev = git_get_hash('HEAD', 'helptags') 530 exec_lua(function() 531 vim.pack.add({ 532 { src = repos_src.basic, version = 'some-tag' }, 533 { src = repos_src.defbranch, version = 'main' }, 534 { src = repos_src.helptags, version = helptags_rev }, 535 { src = repos_src.plugindirs }, 536 { src = repos_src.semver, version = vim.version.range('*') }, 537 }) 538 end) 539 540 local basic_rev = git_get_hash('some-tag', 'basic') 541 local defbranch_rev = git_get_hash('main', 'defbranch') 542 local plugindirs_rev = git_get_hash('HEAD', 'plugindirs') 543 local semver_rev = git_get_hash('v1.0.0', 'semver') 544 545 -- Should properly format as indented JSON. Notes: 546 -- - Branch, tag, and commit should be serialized like `'value'` to be 547 -- distinguishable from version ranges. 548 -- - Absent `version` should be missing and not autoresolved. 549 local ref_lockfile_lines = ([[ 550 { 551 "plugins": { 552 "basic": { 553 "rev": "%s", 554 "src": "%s", 555 "version": "'some-tag'" 556 }, 557 "defbranch": { 558 "rev": "%s", 559 "src": "%s", 560 "version": "'main'" 561 }, 562 "helptags": { 563 "rev": "%s", 564 "src": "%s", 565 "version": "'%s'" 566 }, 567 "plugindirs": { 568 "rev": "%s", 569 "src": "%s" 570 }, 571 "semver": { 572 "rev": "%s", 573 "src": "%s", 574 "version": ">=0.0.0" 575 } 576 } 577 }]]):format( 578 basic_rev, 579 repos_src.basic, 580 defbranch_rev, 581 repos_src.defbranch, 582 helptags_rev, 583 repos_src.helptags, 584 helptags_rev, 585 plugindirs_rev, 586 repos_src.plugindirs, 587 semver_rev, 588 repos_src.semver 589 ) 590 eq(vim.text.indent(0, ref_lockfile_lines), fn.readblob(get_lock_path())) 591 end) 592 593 it('updates lockfile', function() 594 vim_pack_add({ repos_src.basic }) 595 local ref_lockfile = { 596 plugins = { 597 basic = { rev = git_get_hash('main', 'basic'), src = repos_src.basic }, 598 }, 599 } 600 eq(ref_lockfile, get_lock_tbl()) 601 602 n.clear() 603 vim_pack_add({ { src = repos_src.basic, version = 'main' } }) 604 605 ref_lockfile.plugins.basic.version = "'main'" 606 eq(ref_lockfile, get_lock_tbl()) 607 end) 608 609 it('uses lockfile during install', function() 610 vim_pack_add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch }) 611 612 -- Mock clean initial install, but with lockfile present 613 vim.fs.rm(pack_get_dir(), { force = true, recursive = true }) 614 n.clear() 615 watch_events({ 'PackChangedPre', 'PackChanged' }) 616 617 local basic_rev = git_get_hash('feat-branch', 'basic') 618 local defbranch_rev = git_get_hash('HEAD', 'defbranch') 619 local ref_lockfile = { 620 plugins = { 621 basic = { rev = basic_rev, src = repos_src.basic, version = "'feat-branch'" }, 622 defbranch = { rev = defbranch_rev, src = repos_src.defbranch }, 623 }, 624 } 625 eq(ref_lockfile, get_lock_tbl()) 626 627 mock_confirm(1) 628 -- Should use revision from lockfile (pointing at latest 'feat-branch' 629 -- commit) and not use latest `main` commit 630 vim_pack_add({ { src = repos_src.basic, version = 'main' } }) 631 pack_assert_content('basic', 'return "basic feat-branch"') 632 633 local confirm_log = exec_lua('return _G.confirm_log') 634 eq(1, #confirm_log) 635 matches('basic.*defbranch', confirm_log[1][1]) 636 637 -- Should install `defbranch` (as it is in lockfile), but not load it 638 eq(true, pack_exists('defbranch')) 639 eq(false, exec_lua('return pcall(require, "defbranch")')) 640 641 -- Should trigger `kind=install` events 642 local log = exec_lua('return _G.event_log') 643 local find_event = make_find_packchanged(log) 644 local installpre_basic = find_event('Pre', 'install', 'basic', 'feat-branch', false) 645 local installpre_defbranch = find_event('Pre', 'install', 'defbranch', nil, false) 646 local install_basic = find_event('', 'install', 'basic', 'feat-branch', false) 647 local install_defbranch = find_event('', 'install', 'defbranch', nil, false) 648 eq(4, #log) 649 eq(true, installpre_basic < install_basic) 650 eq(true, installpre_defbranch < install_defbranch) 651 652 -- Running `update()` should still update to use `main` 653 exec_lua(function() 654 vim.pack.update({ 'basic' }, { force = true }) 655 end) 656 pack_assert_content('basic', 'return "basic main"') 657 658 ref_lockfile.plugins.basic.rev = git_get_hash('main', 'basic') 659 ref_lockfile.plugins.basic.version = "'main'" 660 eq(ref_lockfile, get_lock_tbl()) 661 end) 662 663 it('handles lockfile during install errors', function() 664 local repo_not_exist = 'file://' .. repo_get_path('does-not-exist') 665 pcall_err(exec_lua, function() 666 vim.pack.add({ 667 repo_not_exist, 668 { src = repos_src.basic, version = 'not-exist' }, 669 { src = repos_src.pluginerr, version = 'main' }, 670 }) 671 end) 672 673 local pluginerr_hash = git_get_hash('main', 'pluginerr') 674 local ref_lockfile = { 675 -- Should be no entry for `repo_not_exist` and `basic` as they did not 676 -- fully install 677 plugins = { 678 -- Error during sourcing 'plugin/' should not affect lockfile 679 pluginerr = { rev = pluginerr_hash, src = repos_src.pluginerr, version = "'main'" }, 680 }, 681 } 682 eq(ref_lockfile, get_lock_tbl()) 683 end) 684 685 it('regenerates manually deleted lockfile', function() 686 vim_pack_add({ 687 { src = repos_src.basic, name = 'other', version = 'feat-branch' }, 688 repos_src.defbranch, 689 }) 690 local lock_path = get_lock_path() 691 eq(true, vim.uv.fs_stat(lock_path) ~= nil) 692 693 local basic_rev = git_get_hash('feat-branch', 'basic') 694 local plugindirs_rev = git_get_hash('dev', 'defbranch') 695 696 -- Should try its best to regenerate lockfile based on installed plugins 697 fn.delete(get_lock_path()) 698 n.clear() 699 vim_pack_add({}) 700 local ref_lockfile = { 701 plugins = { 702 -- No `version = 'feat-branch'` as there is no way to get that info 703 -- (lockfile was the only source of that on disk) 704 other = { rev = basic_rev, src = repos_src.basic }, 705 defbranch = { rev = plugindirs_rev, src = repos_src.defbranch }, 706 }, 707 } 708 eq(ref_lockfile, get_lock_tbl()) 709 710 local ref_messages = 'vim.pack: Repaired corrupted lock data for plugins: defbranch, other' 711 eq(ref_messages, n.exec_capture('messages')) 712 713 -- Calling `add()` with `version` should still add it to lockfile 714 vim_pack_add({ { src = repos_src.basic, name = 'other', version = 'feat-branch' } }) 715 eq("'feat-branch'", get_lock_tbl().plugins.other.version) 716 end) 717 718 it('repairs corrupted lock data for installed plugins', function() 719 vim_pack_add({ 720 -- Should preserve present `version` 721 { src = repos_src.basic, version = 'feat-branch' }, 722 repos_src.defbranch, 723 repos_src.semver, 724 repos_src.helptags, 725 }) 726 727 local lock_tbl = get_lock_tbl() 728 local ref_lock_tbl = vim.deepcopy(lock_tbl) 729 local assert = function() 730 vim_pack_add({}) 731 eq(ref_lock_tbl, get_lock_tbl()) 732 eq(true, pack_exists('basic')) 733 eq(true, pack_exists('defbranch')) 734 eq(true, pack_exists('semver')) 735 eq(true, pack_exists('helptags')) 736 end 737 738 -- Missing lock data required field 739 lock_tbl.plugins.basic.rev = nil 740 -- Wrong lock data field type 741 lock_tbl.plugins.defbranch.src = 1 ---@diagnostic disable-line: assign-type-mismatch 742 -- Wrong lock data type 743 lock_tbl.plugins.semver = 1 ---@diagnostic disable-line: assign-type-mismatch 744 745 local lockfile_text = vim.json.encode(lock_tbl, { indent = ' ', sort_keys = true }) 746 fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path()) 747 748 n.clear() 749 assert() 750 751 local ref_messages = 752 'vim.pack: Repaired corrupted lock data for plugins: basic, defbranch, semver' 753 eq(ref_messages, n.exec_capture('messages')) 754 755 -- Should work even for badly corrupted lockfile 756 lockfile_text = vim.json.encode({ plugins = 1 }, { indent = ' ', sort_keys = true }) 757 fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path()) 758 759 n.clear() 760 -- Can not preserve `version` if it was deleted from the lockfile 761 ref_lock_tbl.plugins.basic.version = nil 762 assert() 763 end) 764 765 it('removes unrepairable corrupted data and plugins', function() 766 vim_pack_add({ repos_src.basic, repos_src.defbranch, repos_src.semver, repos_src.helptags }) 767 768 local lock_tbl = get_lock_tbl() 769 local ref_lock_tbl = vim.deepcopy(lock_tbl) 770 771 -- Corrupted data for missing plugin 772 vim.fs.rm(pack_get_plug_path('basic'), { recursive = true, force = true }) 773 lock_tbl.plugins.basic.rev = nil 774 775 -- Good data for corrupted plugin 776 local defbranch_path = pack_get_plug_path('defbranch') 777 vim.fs.rm(defbranch_path, { recursive = true, force = true }) 778 fn.writefile({ 'File and not directory' }, defbranch_path) 779 780 -- Corrupted data for corrupted plugin 781 local semver_path = pack_get_plug_path('semver') 782 vim.fs.rm(semver_path, { recursive = true, force = true }) 783 fn.writefile({ 'File and not directory' }, semver_path) 784 lock_tbl.plugins.semver.rev = 1 ---@diagnostic disable-line: assign-type-mismatch 785 786 local lockfile_text = vim.json.encode(lock_tbl, { indent = ' ', sort_keys = true }) 787 fn.writefile(vim.split(lockfile_text, '\n'), get_lock_path()) 788 789 n.clear() 790 vim_pack_add({}) 791 ref_lock_tbl.plugins.basic = nil 792 ref_lock_tbl.plugins.defbranch = nil 793 ref_lock_tbl.plugins.semver = nil 794 eq(ref_lock_tbl, get_lock_tbl()) 795 796 eq(false, pack_exists('basic')) 797 eq(false, pack_exists('defbranch')) 798 eq(false, pack_exists('semver')) 799 eq(true, pack_exists('helptags')) 800 end) 801 802 it('installs at proper version', function() 803 local out = exec_lua(function() 804 vim.pack.add({ { src = repos_src.basic, version = 'feat-branch' } }) 805 -- Should have plugin available immediately (not even on the next loop) 806 return require('basic') 807 end) 808 809 eq('basic feat-branch', out) 810 811 local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths()) 812 local plug_path = pack_get_plug_path('basic') 813 local after_dir = vim.fs.joinpath(plug_path, 'after') 814 eq(true, vim.tbl_contains(rtp, plug_path)) 815 -- No 'after/' directory in runtimepath because it is not present in plugin 816 eq(false, vim.tbl_contains(rtp, after_dir)) 817 end) 818 819 it('installs with submodules', function() 820 mock_git_file_transport() 821 vim_pack_add({ repos_src.with_subs }) 822 823 local sub_lua_file = vim.fs.joinpath(pack_get_plug_path('with_subs'), 'sub', 'sub.lua') 824 eq('return "sub main"', fn.readblob(sub_lua_file)) 825 end) 826 827 it('does not install on bad `version`', function() 828 local err = pcall_err(exec_lua, function() 829 vim.pack.add({ { src = repos_src.basic, version = 'not-exist' } }) 830 end) 831 matches('`not%-exist` is not a branch/tag/commit', err) 832 eq(false, pack_exists('basic')) 833 end) 834 835 it('can install from the Internet', function() 836 t.skip(skip_integ, 'NVIM_TEST_INTEG not set: skipping network integration test') 837 vim_pack_add({ 'https://github.com/neovim/nvim-lspconfig' }) 838 eq(true, exec_lua('return pcall(require, "lspconfig")')) 839 end) 840 841 describe('startup', function() 842 local config_dir, pack_add_cmd = '', '' 843 844 before_each(function() 845 config_dir = fn.stdpath('config') 846 fn.mkdir(vim.fs.joinpath(config_dir, 'plugin'), 'p') 847 848 pack_add_cmd = ('vim.pack.add({ %s })'):format(vim.inspect(repos_src.plugindirs)) 849 end) 850 851 after_each(function() 852 vim.fs.rm(config_dir, { recursive = true, force = true }) 853 end) 854 855 local function assert_loaded() 856 eq('plugindirs main', exec_lua('return require("plugindirs")')) 857 858 -- Should source 'plugin/' and 'after/plugin/' exactly once 859 eq({ true, true }, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) 860 eq({ 'p', 'a' }, n.exec_lua('return _G.DL')) 861 end 862 863 local function assert_works() 864 -- Should auto-install but wait before executing code after it 865 n.clear({ args_rm = { '-u' } }) 866 t.retry(nil, 2000, function() 867 eq(true, exec_lua('return _G.done')) 868 end) 869 assert_loaded() 870 871 -- Should only `:packadd!`/`:packadd` already installed plugin 872 n.clear({ args_rm = { '-u' } }) 873 assert_loaded() 874 end 875 876 it('works in init.lua', function() 877 local init_lua = vim.fs.joinpath(config_dir, 'init.lua') 878 fn.writefile({ pack_add_cmd, '_G.done = true' }, init_lua) 879 assert_works() 880 881 -- Should not load plugins if `--noplugin`, only adjust 'runtimepath' 882 n.clear({ args = { '--noplugin' }, args_rm = { '-u' } }) 883 eq('plugindirs main', exec_lua('return require("plugindirs")')) 884 eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) 885 eq(vim.NIL, n.exec_lua('return _G.DL')) 886 end) 887 888 it('works in plugin/', function() 889 local plugin_file = vim.fs.joinpath(config_dir, 'plugin', 'mine.lua') 890 fn.writefile({ pack_add_cmd, '_G.done = true' }, plugin_file) 891 -- Should source plugin's 'plugin/' files without explicit `load=true` 892 assert_works() 893 end) 894 end) 895 896 it('shows progress report during installation', function() 897 track_nvim_echo() 898 vim_pack_add({ repos_src.basic, repos_src.defbranch }) 899 assert_progress_report('Installing plugins', { 'basic', 'defbranch' }) 900 end) 901 902 it('triggers relevant events', function() 903 watch_events({ 'PackChangedPre', 'PackChanged' }) 904 905 -- Should provide event-data respecting manual `version` without inferring default 906 vim_pack_add({ { src = repos_src.basic, version = 'feat-branch' }, repos_src.defbranch }) 907 908 local log = exec_lua('return _G.event_log') 909 local find_event = make_find_packchanged(log) 910 local installpre_basic = find_event('Pre', 'install', 'basic', 'feat-branch', false) 911 local installpre_defbranch = find_event('Pre', 'install', 'defbranch', nil, false) 912 local install_basic = find_event('', 'install', 'basic', 'feat-branch', false) 913 local install_defbranch = find_event('', 'install', 'defbranch', nil, false) 914 eq(4, #log) 915 916 -- NOTE: There is no guaranteed installation order among separate plugins (as it is async) 917 eq(true, installpre_basic < install_basic) 918 eq(true, installpre_defbranch < install_defbranch) 919 end) 920 921 it('recognizes several `version` types', function() 922 local prev_commit = git_get_hash('HEAD~', 'defbranch') 923 exec_lua(function() 924 vim.pack.add({ 925 { src = repos_src.basic, version = 'some-tag' }, -- Tag 926 { src = repos_src.defbranch, version = prev_commit }, -- Commit hash 927 { src = repos_src.semver, version = vim.version.range('<1') }, -- Semver constraint 928 }) 929 end) 930 931 eq('basic some-tag', exec_lua('return require("basic")')) 932 eq('defbranch main', exec_lua('return require("defbranch")')) 933 eq('semver v0.4', exec_lua('return require("semver")')) 934 end) 935 936 it('respects plugin/ and after/plugin/ scripts', function() 937 local function assert(load, ref) 938 vim_pack_add({ { src = repos_src.plugindirs, name = 'plugin % dirs' } }, { load = load }) 939 -- Should handle bad plugin directory name 940 local out = exec_lua(function() 941 return { 942 vim.g._plugin, 943 vim.g._plugin_vim, 944 vim.g._plugin_sub, 945 vim.g._plugin_bad, 946 vim.g._after_plugin, 947 vim.g._after_plugin_vim, 948 vim.g._after_plugin_sub, 949 vim.g._after_plugin_bad, 950 } 951 end) 952 eq(ref, out) 953 954 -- Should add necessary directories to runtimepath regardless of `opts.load` 955 local rtp = vim.tbl_map(t.fix_slashes, api.nvim_list_runtime_paths()) 956 local plug_path = pack_get_plug_path('plugin % dirs') 957 local after_dir = vim.fs.joinpath(plug_path, 'after') 958 eq(true, vim.tbl_contains(rtp, plug_path)) 959 eq(true, vim.tbl_contains(rtp, after_dir)) 960 end 961 962 assert(nil, { true, true, true, true, true, true, true, true }) 963 964 n.clear() 965 assert(false, {}) 966 end) 967 968 it('can use function `opts.load`', function() 969 local function assert() 970 n.exec_lua(function() 971 _G.load_log = {} 972 local function load(...) 973 table.insert(_G.load_log, { ... }) 974 end 975 vim.pack.add({ repos_src.plugindirs, repos_src.basic }, { load = load }) 976 end) 977 978 -- Order of execution should be the same as supplied in `add()` 979 local plugindirs_data = { 980 spec = { src = repos_src.plugindirs, name = 'plugindirs' }, 981 path = pack_get_plug_path('plugindirs'), 982 } 983 local basic_data = { 984 spec = { src = repos_src.basic, name = 'basic' }, 985 path = pack_get_plug_path('basic'), 986 } 987 -- - Only single table argument should be supplied to `load` 988 local ref_log = { { plugindirs_data }, { basic_data } } 989 eq(ref_log, n.exec_lua('return _G.load_log')) 990 991 -- Should not add plugin to the session in any way 992 eq(false, exec_lua('return pcall(require, "plugindirs")')) 993 eq(false, exec_lua('return pcall(require, "basic")')) 994 995 -- Should not source 'plugin/' 996 eq({}, n.exec_lua('return { vim.g._plugin, vim.g._after_plugin }')) 997 998 -- Plugins should still be marked as "active", since they were added 999 eq(true, exec_lua('return vim.pack.get({ "plugindirs" })[1].active')) 1000 eq(true, exec_lua('return vim.pack.get({ "basic" })[1].active')) 1001 end 1002 1003 -- Works on initial install 1004 assert() 1005 1006 -- Works when loading already installed plugin 1007 n.clear() 1008 assert() 1009 end) 1010 1011 it('generates help tags', function() 1012 vim_pack_add({ { src = repos_src.helptags, name = 'help tags' } }) 1013 local target_tags = fn.getcompletion('my-test', 'help') 1014 table.sort(target_tags) 1015 eq({ 'my-test-help', 'my-test-help-bad', 'my-test-help-sub-bad' }, target_tags) 1016 end) 1017 1018 it('reports install/load errors after loading all input', function() 1019 t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'") 1020 local function assert(err_pat) 1021 local err = pcall_err(exec_lua, function() 1022 vim.pack.add({ 1023 { src = repos_src.basic, version = 'wrong-version' }, -- Error during initial checkout 1024 { src = repos_src.semver, version = vim.version.range('>=2.0.0') }, -- Missing version 1025 { src = repos_src.plugindirs, version = 'main' }, 1026 { src = repos_src.pluginerr, version = 'main' }, -- Error during 'plugin/' source 1027 }) 1028 end) 1029 1030 matches(err_pat, err) 1031 1032 -- Should have processed non-errored 'plugin/' and add to 'rtp' 1033 eq('plugindirs main', exec_lua('return require("plugindirs")')) 1034 eq(true, exec_lua('return vim.g._plugin')) 1035 1036 -- Should add plugin to 'rtp' even if 'plugin/' has error 1037 eq('pluginerr main', exec_lua('return require("pluginerr")')) 1038 end 1039 1040 -- During initial install 1041 local err_pat_parts = { 1042 'vim%.pack', 1043 '`basic`:\n', 1044 -- Should report available branches and tags if revision is absent 1045 '`wrong%-version`', 1046 -- Should list default branch first 1047 'Available:\nTags: some%-tag\nBranches: main, feat%-branch', 1048 -- Should report available branches and versions if no constraint match 1049 '`semver`', 1050 'Available:\nVersions: v1%.0%.0, v0%.4, 0%.3%.1, v0%.3%.0.*\nBranches: main\n', 1051 '`pluginerr`:\n', 1052 'Wow, an error', 1053 } 1054 assert(table.concat(err_pat_parts, '.*')) 1055 1056 -- During loading already installed plugin. 1057 n.clear() 1058 -- NOTE: There is no error for wrong `version`, because there is no check 1059 -- for already installed plugins. Might change in the future. 1060 assert('vim%.pack.*`pluginerr`:\n.*Wow, an error') 1061 end) 1062 1063 it('normalizes each spec', function() 1064 vim_pack_add({ 1065 repos_src.basic, -- String should be inferred as `{ src = ... }` 1066 { src = repos_src.defbranch }, -- Default `version` is remote's default branch 1067 { src = repos_src['gitsuffix.git'] }, -- Default `name` comes from `src` repo name 1068 { src = repos_src.plugindirs, name = 'plugin/dirs' }, -- Ensure proper directory name 1069 }) 1070 1071 eq('basic main', exec_lua('return require("basic")')) 1072 eq('defbranch dev', exec_lua('return require("defbranch")')) 1073 eq('gitsuffix main', exec_lua('return require("gitsuffix")')) 1074 eq(true, exec_lua('return vim.g._plugin')) 1075 1076 eq(true, pack_exists('gitsuffix')) 1077 eq(true, pack_exists('dirs')) 1078 end) 1079 1080 it('handles problematic names', function() 1081 vim_pack_add({ { src = repos_src.basic, name = 'bad % name' } }) 1082 eq('basic main', exec_lua('return require("basic")')) 1083 end) 1084 1085 it('is not affected by special environment variables', function() 1086 fn.setenv('GIT_WORK_TREE', t.paths.test_source_path) 1087 fn.setenv('GIT_DIR', vim.fs.joinpath(t.paths.test_source_path, '.git')) 1088 local ref_environ = fn.environ() 1089 1090 vim_pack_add({ repos_src.basic }) 1091 eq('basic main', exec_lua('return require("basic")')) 1092 1093 eq(ref_environ, fn.environ()) 1094 end) 1095 1096 it('validates input', function() 1097 local function assert(err_pat, input) 1098 local function add_input() 1099 vim.pack.add(input) 1100 end 1101 matches(err_pat, pcall_err(exec_lua, add_input)) 1102 end 1103 1104 -- Separate spec entries 1105 assert('list', repos_src.basic) 1106 assert('spec:.*table', { 1 }) 1107 assert('spec%.src:.*string', { { src = 1 } }) 1108 assert('spec%.src:.*non%-empty string', { { src = '' } }) 1109 assert('spec%.name:.*string', { { src = repos_src.basic, name = 1 } }) 1110 assert('spec%.name:.*non%-empty string', { { src = repos_src.basic, name = '' } }) 1111 assert( 1112 'spec%.version:.*string or vim%.VersionRange', 1113 { { src = repos_src.basic, version = 1 } } 1114 ) 1115 1116 -- Conflicts in input array 1117 local version_conflict = { 1118 { src = repos_src.basic, version = 'feat-branch' }, 1119 { src = repos_src.basic, version = 'main' }, 1120 } 1121 assert('Conflicting `version` for `basic`.*feat%-branch.*main', version_conflict) 1122 1123 local src_conflict = { 1124 { src = repos_src.basic, name = 'my-plugin' }, 1125 { src = repos_src.semver, name = 'my-plugin' }, 1126 } 1127 assert('Conflicting `src` for `my%-plugin`.*basic.*semver', src_conflict) 1128 end) 1129 end) 1130 1131 describe('update()', function() 1132 -- Tables with hashes used to test confirmation buffer and log content 1133 local hashes --- @type table<string,string> 1134 local short_hashes --- @type table<string,string> 1135 1136 before_each(function() 1137 -- Create a dedicated clean repo for which "push changes" will be mocked 1138 init_test_repo('fetch') 1139 1140 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch init"') 1141 git_add_commit('Initial commit for "fetch"', 'fetch') 1142 1143 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch main"') 1144 git_add_commit('Commit from `main` to be removed', 'fetch') 1145 1146 hashes = { fetch_head = git_get_hash('HEAD', 'fetch') } 1147 short_hashes = { fetch_head = git_get_short_hash('HEAD', 'fetch') } 1148 1149 -- Install initial versions of tested plugins 1150 vim_pack_add({ 1151 { src = repos_src.fetch, version = 'main' }, 1152 { src = repos_src.semver, version = 'v0.3.0' }, 1153 repos_src.defbranch, 1154 }) 1155 n.clear() 1156 1157 -- Mock remote repo update 1158 -- - Force push 1159 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new"') 1160 git_cmd({ 'add', '*' }, 'fetch') 1161 git_cmd({ 'commit', '--amend', '-m', 'Commit to be added 1' }, 'fetch') 1162 1163 -- - Presence of a tag (should be shown in changelog) 1164 git_cmd({ 'tag', 'dev-tag' }, 'fetch') 1165 1166 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 2"') 1167 git_add_commit('Commit to be added 2', 'fetch') 1168 1169 -- Make `dev` default remote branch to check that `version` is respected 1170 git_cmd({ 'checkout', '-b', 'dev' }, 'fetch') 1171 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch dev"') 1172 git_add_commit('Commit from default `dev` branch', 'fetch') 1173 end) 1174 1175 after_each(function() 1176 pcall(vim.fs.rm, repo_get_path('fetch'), { force = true, recursive = true }) 1177 local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') 1178 pcall(vim.fs.rm, log_path, { force = true }) 1179 end) 1180 1181 describe('confirmation buffer', function() 1182 it('works', function() 1183 vim_pack_add({ 1184 repos_src.fetch, 1185 { src = repos_src.semver, version = 'v0.3.0' }, 1186 { src = repos_src.defbranch, version = 'does-not-exist' }, 1187 }) 1188 pack_assert_content('fetch', 'return "fetch main"') 1189 1190 exec_lua(function() 1191 -- Enable highlighting of special filetype 1192 vim.cmd('filetype plugin on') 1193 vim.pack.update() 1194 end) 1195 1196 -- Buffer should be special and shown in a separate tabpage 1197 eq(2, #api.nvim_list_tabpages()) 1198 eq(2, fn.tabpagenr()) 1199 eq(api.nvim_get_option_value('filetype', {}), 'nvim-pack') 1200 eq(api.nvim_get_option_value('modifiable', {}), false) 1201 eq(api.nvim_get_option_value('buftype', {}), 'acwrite') 1202 local confirm_bufnr = api.nvim_get_current_buf() 1203 local confirm_winnr = api.nvim_get_current_win() 1204 local confirm_tabpage = api.nvim_get_current_tabpage() 1205 eq(api.nvim_buf_get_name(0), 'nvim-pack://confirm#' .. confirm_bufnr) 1206 1207 -- Adjust lines for a more robust screenshot testing 1208 local fetch_src = repos_src.fetch 1209 local fetch_path = pack_get_plug_path('fetch') 1210 local semver_src = repos_src.semver 1211 local semver_path = pack_get_plug_path('semver') 1212 local pack_runtime = '/lua/vim/pack.lua' 1213 1214 exec_lua(function() 1215 -- Replace matches in line to preserve extmark highlighting 1216 local function replace_in_line(i, pattern, repl) 1217 local line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] 1218 local from, to = line:find(pattern) 1219 while from and to do 1220 vim.api.nvim_buf_set_text(0, i - 1, from - 1, i - 1, to, { repl }) 1221 line = vim.api.nvim_buf_get_lines(0, i - 1, i, false)[1] 1222 from, to = line:find(pattern) 1223 end 1224 end 1225 1226 vim.bo.modifiable = true 1227 local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) 1228 -- NOTE: replace path to `vim.pack` in error traceback accounting for 1229 -- pcall source truncation and possibly different slashes on Windows 1230 local pack_runtime_pesc = vim.pesc(pack_runtime):gsub('/', '[\\/]') 1231 local pack_runtime_pattern = ('%%S.+%s:%%d+'):format(pack_runtime_pesc) 1232 for i = 1, #lines do 1233 replace_in_line(i, pack_runtime_pattern, 'VIM_PACK_RUNTIME') 1234 replace_in_line(i, vim.pesc(fetch_path), 'FETCH_PATH') 1235 replace_in_line(i, vim.pesc(fetch_src), 'FETCH_SRC') 1236 replace_in_line(i, vim.pesc(semver_path), 'SEMVER_PATH') 1237 replace_in_line(i, vim.pesc(semver_src), 'SEMVER_SRC') 1238 end 1239 vim.bo.modified = false 1240 vim.bo.modifiable = false 1241 end) 1242 1243 -- Use screenshot to test highlighting, otherwise prefer text matching. 1244 -- This requires computing target hashes on each test run because they 1245 -- change due to source repos being cleanly created on each file test. 1246 local screen 1247 screen = Screen.new(85, 35) 1248 1249 hashes.fetch_new = git_get_hash('main', 'fetch') 1250 short_hashes.fetch_new = git_get_short_hash('main', 'fetch') 1251 short_hashes.fetch_new_prev = git_get_short_hash('main~', 'fetch') 1252 hashes.semver_head = git_get_hash('v0.3.0', 'semver') 1253 1254 local tab_name = 'n' .. (t.is_os('win') and ':' or '') .. '//confirm#2' 1255 1256 local ref_screen_lines = ([[ 1257 {24: [No Name] }{5: %s }{2:%s }{24:X}| 1258 {19:^# Error ────────────────────────────────────────────────────────────────────────} | 1259 | 1260 {19:## defbranch} | 1261 | 1262 VIM_PACK_RUNTIME: `does-not-exist` is not a branch/tag/commit. Available: | 1263 Tags: | 1264 Branches: dev, main | 1265 | 1266 {101:# Update ───────────────────────────────────────────────────────────────────────} | 1267 | 1268 {101:## fetch} | 1269 Path: {103:FETCH_PATH} | 1270 Source: {103:FETCH_SRC} | 1271 Revision before: {103:%s} | 1272 Revision after: {103:%s} {102:(main)} | 1273 | 1274 Pending updates: | 1275 {19:< %s │ Commit from `main` to be removed} | 1276 {104:> %s │ Commit to be added 2} | 1277 {104:> %s │ Commit to be added 1 (tag: dev-tag)} | 1278 | 1279 {102:# Same ─────────────────────────────────────────────────────────────────────────} | 1280 | 1281 {102:## semver} | 1282 Path: {103:SEMVER_PATH} | 1283 Source: {103:SEMVER_SRC} | 1284 Revision: {103:%s} {102:(v0.3.0)} | 1285 | 1286 Available newer versions: | 1287 • {102:v1.0.0} | 1288 • {102:v0.4} | 1289 • {102:0.3.1} | 1290 {1:~ }| 1291 | 1292 ]]):format( 1293 tab_name, 1294 t.is_os('win') and '' or ' ', 1295 hashes.fetch_head, 1296 hashes.fetch_new, 1297 short_hashes.fetch_head, 1298 short_hashes.fetch_new, 1299 short_hashes.fetch_new_prev, 1300 hashes.semver_head 1301 ) 1302 1303 screen:add_extra_attr_ids({ 1304 [101] = { foreground = Screen.colors.Orange }, 1305 [102] = { foreground = Screen.colors.LightGray }, 1306 [103] = { foreground = Screen.colors.LightBlue }, 1307 [104] = { foreground = Screen.colors.SeaGreen }, 1308 }) 1309 -- NOTE: Non LuaJIT reports errors differently due to 'coxpcall' 1310 if is_jit() then 1311 local ref_screen = vim.text.indent(0, ref_screen_lines) 1312 screen:expect(ref_screen) 1313 end 1314 1315 -- `:write` should confirm 1316 n.exec('write') 1317 1318 -- - Apply changes immediately 1319 pack_assert_content('fetch', 'return "fetch new 2"') 1320 1321 -- - Clean up buffer+window+tabpage 1322 eq(false, api.nvim_buf_is_valid(confirm_bufnr)) 1323 eq(false, api.nvim_win_is_valid(confirm_winnr)) 1324 eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) 1325 1326 -- - Write to log file 1327 local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') 1328 local log_text = fn.readblob(log_path) 1329 local log_1, log_rest = log_text:match('^(.-)\n(.*)$') --- @type string, string 1330 matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_1) 1331 local ref_log_lines = ([[ 1332 # Update ─────────────────────────────────────────────────────────────────────── 1333 1334 ## fetch 1335 Path: %s 1336 Source: %s 1337 Revision before: %s 1338 Revision after: %s (main) 1339 1340 Pending updates: 1341 < %s │ Commit from `main` to be removed 1342 > %s │ Commit to be added 2 1343 > %s │ Commit to be added 1 (tag: dev-tag)]]):format( 1344 fetch_path, 1345 fetch_src, 1346 hashes.fetch_head, 1347 hashes.fetch_new, 1348 short_hashes.fetch_head, 1349 short_hashes.fetch_new, 1350 short_hashes.fetch_new_prev 1351 ) 1352 eq(vim.text.indent(0, ref_log_lines), vim.trim(log_rest)) 1353 end) 1354 1355 it('can be dismissed with `:quit`', function() 1356 vim_pack_add({ repos_src.fetch }) 1357 exec_lua('vim.pack.update({ "fetch" })') 1358 eq('nvim-pack', api.nvim_get_option_value('filetype', {})) 1359 1360 -- Should not apply updates 1361 n.exec('quit') 1362 pack_assert_content('fetch', 'return "fetch main"') 1363 end) 1364 1365 it('closes full tabpage', function() 1366 vim_pack_add({ repos_src.fetch }) 1367 exec_lua('vim.pack.update()') 1368 1369 -- Confirm with `:write` 1370 local confirm_tabpage = api.nvim_get_current_tabpage() 1371 n.exec('-tab split other-tab') 1372 local other_tabpage = api.nvim_get_current_tabpage() 1373 n.exec('tabnext') 1374 n.exec('write') 1375 eq(true, api.nvim_get_current_tabpage() == other_tabpage) 1376 eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) 1377 1378 -- Not confirm with `:quit` 1379 n.exec('tab split other-tab-2') 1380 local other_tabpage_2 = api.nvim_get_current_tabpage() 1381 exec_lua('vim.pack.update()') 1382 confirm_tabpage = api.nvim_get_current_tabpage() 1383 1384 -- - Temporary split window in tabpage should prevent from closing 1385 n.exec('vsplit other-buf') 1386 n.exec('wincmd w') 1387 1388 n.exec('tabclose ' .. api.nvim_tabpage_get_number(other_tabpage_2)) 1389 eq(confirm_tabpage, api.nvim_get_current_tabpage()) 1390 n.exec('quit') 1391 eq(confirm_tabpage, api.nvim_get_current_tabpage()) 1392 n.exec('quit') 1393 eq(false, api.nvim_tabpage_is_valid(confirm_tabpage)) 1394 1395 -- Should work even if it is the last tabpage 1396 exec_lua('vim.pack.update()') 1397 n.exec('tabonly') 1398 n.exec('write') 1399 eq('', n.eval('v:errmsg')) 1400 1401 -- Should cleanly close tabpage even if there are only scratch buffers 1402 n.exec('%bwipeout') 1403 local init_buf = api.nvim_get_current_buf() 1404 api.nvim_set_current_buf(api.nvim_create_buf(false, true)) 1405 api.nvim_buf_delete(init_buf, { force = true }) 1406 exec_lua('vim.pack.update()') 1407 n.exec('write') 1408 eq(1, #api.nvim_list_tabpages()) 1409 eq(1, #api.nvim_list_bufs()) 1410 end) 1411 1412 it('has in-process LSP features', function() 1413 t.skip(not is_jit(), "Non LuaJIT reports errors differently due to 'coxpcall'") 1414 track_nvim_echo() 1415 vim_pack_add({ 1416 repos_src.fetch, 1417 -- No `semver` to test with non-active plugins 1418 { src = repos_src.defbranch, version = 'does-not-exist' }, 1419 }) 1420 exec_lua('vim.pack.update()') 1421 1422 eq(1, exec_lua('return #vim.lsp.get_clients({ bufnr = 0 })')) 1423 1424 -- textDocument/documentSymbol 1425 exec_lua('vim.lsp.buf.document_symbol()') 1426 local loclist = vim.tbl_map(function(x) --- @param x table 1427 return { 1428 lnum = x.lnum, --- @type integer 1429 col = x.col, --- @type integer 1430 end_lnum = x.end_lnum, --- @type integer 1431 end_col = x.end_col, --- @type integer 1432 text = x.text, --- @type string 1433 } 1434 end, fn.getloclist(0)) 1435 local ref_loclist = { 1436 { lnum = 1, col = 1, end_lnum = 9, end_col = 1, text = '[Namespace] Error' }, 1437 { lnum = 3, col = 1, end_lnum = 9, end_col = 1, text = '[Module] defbranch' }, 1438 { lnum = 9, col = 1, end_lnum = 22, end_col = 1, text = '[Namespace] Update' }, 1439 { lnum = 11, col = 1, end_lnum = 22, end_col = 1, text = '[Module] fetch' }, 1440 { lnum = 22, col = 1, end_lnum = 32, end_col = 1, text = '[Namespace] Same' }, 1441 { lnum = 24, col = 1, end_lnum = 32, end_col = 1, text = '[Module] semver (not active)' }, 1442 } 1443 eq(ref_loclist, loclist) 1444 1445 n.exec('lclose') 1446 1447 -- textDocument/hover 1448 local confirm_winnr = api.nvim_get_current_win() 1449 local function assert_hover(pos, commit_msg) 1450 api.nvim_win_set_cursor(0, pos) 1451 exec_lua(function() 1452 vim.lsp.buf.hover() 1453 -- Default hover is async shown in floating window 1454 vim.wait(1000, function() 1455 return #vim.api.nvim_tabpage_list_wins(0) > 1 1456 end) 1457 end) 1458 1459 local all_wins = api.nvim_tabpage_list_wins(0) 1460 eq(2, #all_wins) 1461 local float_winnr = all_wins[1] == confirm_winnr and all_wins[2] or all_wins[1] 1462 eq(true, api.nvim_win_get_config(float_winnr).relative ~= '') 1463 1464 local float_buf = api.nvim_win_get_buf(float_winnr) 1465 local text = table.concat(api.nvim_buf_get_lines(float_buf, 0, -1, false), '\n') 1466 1467 local ref_pattern = 'Marvim <marvim@neovim%.io>\nDate:.*' .. vim.pesc(commit_msg) 1468 matches(ref_pattern, text) 1469 end 1470 1471 assert_hover({ 14, 0 }, 'Commit from `main` to be removed') 1472 assert_hover({ 15, 0 }, 'Commit to be added 2') 1473 assert_hover({ 18, 0 }, 'Commit from `main` to be removed') 1474 assert_hover({ 19, 0 }, 'Commit to be added 2') 1475 assert_hover({ 20, 0 }, 'Commit to be added 1') 1476 assert_hover({ 27, 0 }, 'Add version v0.3.0') 1477 assert_hover({ 30, 0 }, 'Add version v1.0.0') 1478 assert_hover({ 31, 0 }, 'Add version v0.4') 1479 assert_hover({ 32, 0 }, 'Add version 0.3.1') 1480 1481 -- textDocument/codeAction 1482 n.exec_lua(function() 1483 -- Mock `vim.ui.select()` which is a default code action selection 1484 _G.select_idx = 0 1485 1486 ---@diagnostic disable-next-line: duplicate-set-field 1487 vim.ui.select = function(items, _, on_choice) 1488 _G.select_items = items 1489 local idx = _G.select_idx 1490 if idx > 0 then 1491 on_choice(items[idx], idx) 1492 -- Minor delay before continue because LSP cmd execution is async 1493 vim.wait(10) 1494 end 1495 end 1496 end) 1497 1498 local ref_lockfile = get_lock_tbl() --- @type vim.pack.Lock 1499 1500 local function assert_action(pos, action_titles, select_idx) 1501 api.nvim_win_set_cursor(0, pos) 1502 1503 local lines = api.nvim_buf_get_lines(0, 0, -1, false) 1504 n.exec_lua(function() 1505 _G.select_items = nil 1506 _G.select_idx = select_idx 1507 vim.lsp.buf.code_action() 1508 end) 1509 local titles = vim.tbl_map(function(x) --- @param x table 1510 return x.action.title 1511 end, n.exec_lua('return _G.select_items or {}')) 1512 eq(titles, action_titles) 1513 1514 -- If no action is asked (like via cancel), should not delete lines 1515 if select_idx <= 0 then 1516 eq(lines, api.nvim_buf_get_lines(0, 0, -1, false)) 1517 end 1518 end 1519 1520 -- - Should not include "namespace" header as "plugin at cursor" 1521 assert_action({ 1, 1 }, {}, 0) 1522 assert_action({ 2, 0 }, {}, 0) 1523 -- - No actions for `defbranch` since it is active and has no updates 1524 assert_action({ 3, 1 }, {}, 0) 1525 assert_action({ 7, 0 }, {}, 0) 1526 -- - Should not include separator blank line as "plugin at cursor" 1527 assert_action({ 8, 0 }, {}, 0) 1528 assert_action({ 9, 0 }, {}, 0) 1529 assert_action({ 10, 0 }, {}, 0) 1530 -- - Should suggest updating related actions if updates available 1531 local fetch_actions = { 'Update `fetch`', 'Skip updating `fetch`' } 1532 assert_action({ 11, 0 }, fetch_actions, 0) 1533 assert_action({ 14, 0 }, fetch_actions, 0) 1534 assert_action({ 20, 0 }, fetch_actions, 0) 1535 assert_action({ 21, 0 }, {}, 0) 1536 assert_action({ 22, 0 }, {}, 0) 1537 assert_action({ 23, 0 }, {}, 0) 1538 -- - Only deletion should be available for not active plugins 1539 assert_action({ 24, 0 }, { 'Delete `semver`' }, 0) 1540 assert_action({ 28, 0 }, { 'Delete `semver`' }, 0) 1541 assert_action({ 32, 0 }, { 'Delete `semver`' }, 0) 1542 1543 -- - Should correctly perform action and remove plugin's lines 1544 local function line_match(lnum, pattern) 1545 matches(pattern, api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1]) 1546 end 1547 1548 -- - Delete not active plugin. Should remove from disk and update lockfile. 1549 assert_action({ 24, 0 }, { 'Delete `semver`' }, 1) 1550 eq(false, pack_exists('semver')) 1551 line_match(22, '^# Same') 1552 eq(22, api.nvim_buf_line_count(0)) 1553 1554 ref_lockfile.plugins.semver = nil 1555 eq(ref_lockfile, get_lock_tbl()) 1556 1557 -- - Skip udating 1558 assert_action({ 11, 0 }, fetch_actions, 2) 1559 pack_assert_content('fetch', 'return "fetch main"') 1560 line_match(9, '^# Update') 1561 line_match(10, '^$') 1562 line_match(11, '^# Same') 1563 1564 -- - Update plugin. Should not re-fetch new data and update lockfile. 1565 n.exec('quit') 1566 n.exec_lua(function() 1567 vim.pack.update({ 'fetch' }) 1568 end) 1569 exec_lua('_G.echo_log = {}') 1570 1571 ref_lockfile.plugins.fetch.rev = git_get_hash('main', 'fetch') 1572 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"') 1573 git_add_commit('Commit to be added 3', 'fetch') 1574 1575 assert_action({ 3, 0 }, fetch_actions, 1) 1576 1577 pack_assert_content('fetch', 'return "fetch new 2"') 1578 assert_progress_report('Applying updates', { 'fetch' }) 1579 line_match(1, '^# Update') 1580 eq(1, api.nvim_buf_line_count(0)) 1581 1582 eq(ref_lockfile, get_lock_tbl()) 1583 1584 -- - Can still respect `:write` after action 1585 n.exec('write') 1586 eq('vim.pack: Nothing to update', n.exec_capture('1messages')) 1587 eq(api.nvim_get_option_value('filetype', {}), '') 1588 end) 1589 1590 it('has buffer-local mappings', function() 1591 t.skip(not is_jit(), "Non LuaJIT reports update errors differently due to 'coxpcall'") 1592 vim_pack_add({ 1593 repos_src.fetch, 1594 { src = repos_src.semver, version = 'v0.3.0' }, 1595 { src = repos_src.defbranch, version = 'does-not-exist' }, 1596 }) 1597 -- Enable sourcing filetype script (that creates mappings) 1598 n.exec('filetype plugin on') 1599 exec_lua('vim.pack.update()') 1600 1601 -- Plugin sections navigation 1602 local function assert(keys, ref_cursor) 1603 n.feed(keys) 1604 eq(ref_cursor, api.nvim_win_get_cursor(0)) 1605 end 1606 1607 api.nvim_win_set_cursor(0, { 1, 1 }) 1608 assert(']]', { 3, 0 }) 1609 assert(']]', { 11, 0 }) 1610 assert(']]', { 24, 0 }) 1611 -- - Should not wrap around the edge 1612 assert(']]', { 24, 0 }) 1613 1614 api.nvim_win_set_cursor(0, { 32, 1 }) 1615 assert('[[', { 24, 0 }) 1616 assert('[[', { 11, 0 }) 1617 assert('[[', { 3, 0 }) 1618 -- - Should not wrap around the edge 1619 assert('[[', { 3, 0 }) 1620 end) 1621 1622 it('suggests newer versions when on non-tagged commit', function() 1623 local commit = git_get_hash('0.3.1~', 'semver') 1624 1625 -- Make fresh install for cleaner test 1626 exec_lua('vim.pack.del({ "semver" })') 1627 vim_pack_add({ { src = repos_src.semver, version = commit } }) 1628 exec_lua('vim.pack.update({ "semver" })') 1629 1630 -- Should correctly infer that 0.3.0 is the latest version and suggest 1631 -- versions greater than that 1632 local confirm_text = table.concat(api.nvim_buf_get_lines(0, 0, -1, false), '\n') 1633 matches('Available newer versions:\n• v1%.0%.0\n• v0%.4\n• 0%.3%.1$', confirm_text) 1634 end) 1635 1636 it('updates lockfile', function() 1637 vim_pack_add({ repos_src.fetch }) 1638 local ref_fetch_lock = { rev = hashes.fetch_head, src = repos_src.fetch } 1639 eq(ref_fetch_lock, get_lock_tbl().plugins.fetch) 1640 1641 exec_lua('vim.pack.update()') 1642 n.exec('write') 1643 1644 ref_fetch_lock.rev = git_get_hash('main', 'fetch') 1645 eq(ref_fetch_lock, get_lock_tbl().plugins.fetch) 1646 end) 1647 1648 it('hints about not active plugins', function() 1649 exec_lua(function() 1650 vim.pack.update() 1651 end) 1652 1653 for _, l in ipairs(api.nvim_buf_get_lines(0, 0, -1, false)) do 1654 if l:match('^## ') then 1655 matches(' %(not active%)$', l) 1656 end 1657 end 1658 1659 -- Should also hint in `textDocument/documentSymbol` of in-process LSP, 1660 -- yet still work for navigation 1661 exec_lua('vim.lsp.buf.document_symbol()') 1662 local loclist = fn.getloclist(0) 1663 matches(' %(not active%)$', loclist[2].text) 1664 matches(' %(not active%)$', loclist[4].text) 1665 matches(' %(not active%)$', loclist[5].text) 1666 1667 n.exec('llast') 1668 eq(21, api.nvim_win_get_cursor(0)[1]) 1669 end) 1670 end) 1671 1672 it('works with not active plugins', function() 1673 -- No plugins are added, but they are installed in `before_each()` 1674 exec_lua(function() 1675 -- By default should also include not active plugins 1676 vim.pack.update() 1677 end) 1678 pack_assert_content('fetch', 'return "fetch main"') 1679 n.exec('write') 1680 pack_assert_content('fetch', 'return "fetch new 2"') 1681 end) 1682 1683 it('works with submodules', function() 1684 mock_git_file_transport() 1685 vim_pack_add({ { src = repos_src.with_subs, version = 'init-commit' } }) 1686 1687 local sub_lua_file = vim.fs.joinpath(pack_get_plug_path('with_subs'), 'sub', 'sub.lua') 1688 eq('return "sub init"', fn.readblob(sub_lua_file)) 1689 1690 n.clear() 1691 mock_git_file_transport() 1692 vim_pack_add({ repos_src.with_subs }) 1693 exec_lua('vim.pack.update({ "with_subs" })') 1694 n.exec('write') 1695 eq('return "sub main"', fn.readblob(sub_lua_file)) 1696 end) 1697 1698 it('can force update', function() 1699 vim_pack_add({ repos_src.fetch }) 1700 exec_lua('vim.pack.update({ "fetch" }, { force = true })') 1701 1702 -- Apply changes immediately 1703 local fetch_src = repos_src.fetch 1704 local fetch_path = pack_get_plug_path('fetch') 1705 pack_assert_content('fetch', 'return "fetch new 2"') 1706 1707 -- No special buffer/window/tabpage 1708 eq(1, #api.nvim_list_tabpages()) 1709 eq(1, #api.nvim_list_wins()) 1710 eq('', api.nvim_get_option_value('filetype', {})) 1711 1712 -- Write to log file 1713 hashes.fetch_new = git_get_hash('main', 'fetch') 1714 short_hashes.fetch_new = git_get_short_hash('main', 'fetch') 1715 short_hashes.fetch_new_prev = git_get_short_hash('main~', 'fetch') 1716 1717 local log_path = vim.fs.joinpath(fn.stdpath('log'), 'nvim-pack.log') 1718 local log_text = fn.readblob(log_path) 1719 local log_1, log_rest = log_text:match('^(.-)\n(.*)$') --- @type string, string 1720 matches('========== Update %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d ==========', log_1) 1721 local ref_log_lines = ([[ 1722 # Update ─────────────────────────────────────────────────────────────────────── 1723 1724 ## fetch 1725 Path: %s 1726 Source: %s 1727 Revision before: %s 1728 Revision after: %s (main) 1729 1730 Pending updates: 1731 < %s │ Commit from `main` to be removed 1732 > %s │ Commit to be added 2 1733 > %s │ Commit to be added 1 (tag: dev-tag)]]):format( 1734 fetch_path, 1735 fetch_src, 1736 hashes.fetch_head, 1737 hashes.fetch_new, 1738 short_hashes.fetch_head, 1739 short_hashes.fetch_new, 1740 short_hashes.fetch_new_prev 1741 ) 1742 eq(vim.text.indent(0, ref_log_lines), vim.trim(log_rest)) 1743 1744 -- Should update lockfile 1745 eq(hashes.fetch_new, get_lock_tbl().plugins.fetch.rev) 1746 end) 1747 1748 it('can use lockfile revision as a target', function() 1749 vim_pack_add({ repos_src.fetch }) 1750 pack_assert_content('fetch', 'return "fetch main"') 1751 1752 -- Mock "update -> revert lockfile -> revert plugin" 1753 local lock_path = get_lock_path() 1754 local lockfile_before = fn.readblob(lock_path) 1755 hashes.fetch_new = git_get_hash('main', 'fetch') 1756 1757 -- - Update 1758 exec_lua('vim.pack.update({ "fetch" }, { force = true })') 1759 pack_assert_content('fetch', 'return "fetch new 2"') 1760 1761 -- - Revert lockfile 1762 fn.writefile(vim.split(lockfile_before, '\n'), lock_path) 1763 n.clear() 1764 1765 -- - Revert plugin 1766 pack_assert_content('fetch', 'return "fetch new 2"') 1767 exec_lua('vim.pack.update({ "fetch" }, { target = "lockfile" })') 1768 local confirm_lines = api.nvim_buf_get_lines(0, 0, -1, false) 1769 n.exec('write') 1770 pack_assert_content('fetch', 'return "fetch main"') 1771 eq(hashes.fetch_head, get_lock_tbl().plugins.fetch.rev) 1772 1773 -- - Should mention that new revision comes from *lockfile* 1774 eq(confirm_lines[6], ('Revision before: %s'):format(hashes.fetch_new)) 1775 eq(confirm_lines[7], ('Revision after: %s (*lockfile*)'):format(hashes.fetch_head)) 1776 end) 1777 1778 it('can change `src` of installed plugin', function() 1779 local basic_src = repos_src.basic 1780 local defbranch_src = repos_src.defbranch 1781 vim_pack_add({ basic_src }) 1782 1783 local function assert_origin(ref) 1784 -- Should be in sync both on disk and in lockfile 1785 local opts = { cwd = pack_get_plug_path('basic') } 1786 local real_origin = system_sync({ 'git', 'remote', 'get-url', 'origin' }, opts) 1787 eq(ref, vim.trim(real_origin.stdout)) 1788 1789 eq(ref, get_lock_tbl().plugins.basic.src) 1790 end 1791 1792 n.clear() 1793 watch_events({ 'PackChangedPre', 'PackChanged' }) 1794 1795 assert_origin(basic_src) 1796 vim_pack_add({ { src = defbranch_src, name = 'basic' } }) 1797 -- Should not yet (after `add()`) affect plugin source 1798 assert_origin(basic_src) 1799 1800 -- Should update source immediately (to work if updates are discarded) 1801 exec_lua(function() 1802 vim.pack.update({ 'basic' }) 1803 end) 1804 assert_origin(defbranch_src) 1805 1806 -- Should not revert source change even if update is discarded 1807 n.exec('quit') 1808 assert_origin(defbranch_src) 1809 eq({}, exec_lua('return _G.event_log')) 1810 1811 -- Should work with forced update 1812 n.clear() 1813 vim_pack_add({ basic_src }) 1814 exec_lua('vim.pack.update({ "basic" }, { force = true })') 1815 assert_origin(basic_src) 1816 end) 1817 1818 it('can do offline update', function() 1819 vim_pack_add({ { src = repos_src.defbranch, version = 'main' } }) 1820 track_nvim_echo() 1821 1822 pack_assert_content('defbranch', 'return "defbranch dev"') 1823 n.exec_lua(function() 1824 vim.pack.update({ 'defbranch' }, { offline = true }) 1825 end) 1826 1827 -- There should be no progress report about downloading updates 1828 assert_progress_report('Computing updates', { 'defbranch' }) 1829 1830 n.exec('write') 1831 pack_assert_content('defbranch', 'return "defbranch main"') 1832 end) 1833 1834 it('shows progress report', function() 1835 track_nvim_echo() 1836 vim_pack_add({ repos_src.fetch, repos_src.defbranch }) 1837 -- Should also include updates from not active plugins 1838 exec_lua('vim.pack.update()') 1839 1840 -- During initial download 1841 assert_progress_report('Downloading updates', { 'fetch', 'defbranch', 'semver' }) 1842 exec_lua('_G.echo_log = {}') 1843 1844 -- During application (only for plugins that have updates) 1845 n.exec('write') 1846 assert_progress_report('Applying updates', { 'fetch' }) 1847 1848 -- During force update 1849 n.clear() 1850 track_nvim_echo() 1851 repo_write_file('fetch', 'lua/fetch.lua', 'return "fetch new 3"') 1852 git_add_commit('Commit to be added 3', 'fetch') 1853 1854 vim_pack_add({ repos_src.fetch, repos_src.defbranch }) 1855 exec_lua('vim.pack.update(nil, { force = true })') 1856 assert_progress_report('Updating', { 'fetch', 'defbranch', 'semver' }) 1857 end) 1858 1859 it('triggers relevant events', function() 1860 watch_events({ 'PackChangedPre', 'PackChanged' }) 1861 vim_pack_add({ repos_src.fetch, repos_src.defbranch }) 1862 exec_lua('_G.event_log = {}') 1863 exec_lua('vim.pack.update()') 1864 eq({}, exec_lua('return _G.event_log')) 1865 1866 -- Should trigger relevant events only for actually updated plugins 1867 n.exec('write') 1868 local log = exec_lua('return _G.event_log') 1869 local find_event = make_find_packchanged(log) 1870 eq(1, find_event('Pre', 'update', 'fetch', nil, true)) 1871 eq(2, find_event('', 'update', 'fetch', nil, true)) 1872 eq(2, #log) 1873 end) 1874 1875 it('stashes before applying changes', function() 1876 local fetch_lua_file = vim.fs.joinpath(pack_get_plug_path('fetch'), 'lua', 'fetch.lua') 1877 fn.writefile({ 'A text that will be stashed' }, fetch_lua_file) 1878 1879 vim_pack_add({ repos_src.fetch }) 1880 exec_lua('vim.pack.update()') 1881 n.exec('write') 1882 1883 local fetch_path = pack_get_plug_path('fetch') 1884 local stash_list = system_sync({ 'git', 'stash', 'list' }, { cwd = fetch_path }).stdout or '' 1885 matches('vim%.pack: %d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d Stash before checkout', stash_list) 1886 1887 -- Update should still be applied 1888 pack_assert_content('fetch', 'return "fetch new 2"') 1889 end) 1890 1891 it('is not affected by special environment variables', function() 1892 fn.setenv('GIT_WORK_TREE', t.paths.test_source_path) 1893 fn.setenv('GIT_DIR', vim.fs.joinpath(t.paths.test_source_path, '.git')) 1894 local ref_environ = fn.environ() 1895 1896 vim_pack_add({ repos_src.fetch }) 1897 exec_lua('vim.pack.update({ "fetch" }, { force = true })') 1898 pack_assert_content('fetch', 'return "fetch new 2"') 1899 1900 eq(ref_environ, fn.environ()) 1901 end) 1902 1903 it('works with out of sync lockfile', function() 1904 -- Should first autoinstall missing plugin (with confirmation) 1905 vim.fs.rm(pack_get_plug_path('fetch'), { force = true, recursive = true }) 1906 n.clear() 1907 mock_confirm(1) 1908 exec_lua(function() 1909 vim.pack.update(nil, { force = true }) 1910 end) 1911 eq(1, exec_lua('return #_G.confirm_log')) 1912 -- - Should checkout `version='main'` as it says in the lockfile 1913 pack_assert_content('fetch', 'return "fetch new 2"') 1914 1915 -- Should regenerate absent lockfile (from present plugins) 1916 vim.fs.rm(get_lock_path()) 1917 n.clear() 1918 exec_lua(function() 1919 vim.pack.update(nil, { force = true }) 1920 end) 1921 local lock_plugins = get_lock_tbl().plugins 1922 eq(3, vim.tbl_count(lock_plugins)) 1923 -- - Should checkout default branch since `version='main'` info is lost 1924 -- after lockfile is deleted. 1925 eq(nil, lock_plugins.fetch.version) 1926 pack_assert_content('fetch', 'return "fetch dev"') 1927 end) 1928 1929 it('validates input', function() 1930 local function assert(err_pat, input) 1931 local function update_input() 1932 vim.pack.update(input) 1933 end 1934 matches(err_pat, pcall_err(exec_lua, update_input)) 1935 end 1936 1937 assert('list', 1) 1938 1939 -- Should first check if every plugin name represents installed plugin 1940 -- If not - stop early before any update 1941 vim_pack_add({ repos_src.basic }) 1942 1943 assert('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' }) 1944 1945 -- Empty list is allowed with warning 1946 n.exec('messages clear') 1947 exec_lua(function() 1948 vim.pack.update({}) 1949 end) 1950 eq('vim.pack: Nothing to update', n.exec_capture('messages')) 1951 end) 1952 end) 1953 1954 describe('get()', function() 1955 local function make_basic_data(active, info) 1956 local spec = { name = 'basic', src = repos_src.basic, version = 'feat-branch' } 1957 local path = pack_get_plug_path('basic') 1958 local rev = git_get_hash('feat-branch', 'basic') 1959 local res = { active = active, path = path, spec = spec, rev = rev } 1960 if info then 1961 res.branches = { 'main', 'feat-branch' } 1962 res.tags = { 'some-tag' } 1963 end 1964 return res 1965 end 1966 1967 local function make_defbranch_data(active, info) 1968 local spec = { name = 'defbranch', src = repos_src.defbranch } 1969 local path = pack_get_plug_path('defbranch') 1970 local rev = git_get_hash('dev', 'defbranch') 1971 local res = { active = active, path = path, spec = spec, rev = rev } 1972 if info then 1973 res.branches = { 'dev', 'main' } 1974 res.tags = {} 1975 end 1976 return res 1977 end 1978 1979 local function make_plugindirs_data(active, info) 1980 local spec = 1981 { name = 'plugindirs', src = repos_src.plugindirs, version = vim.version.range('*') } 1982 local path = pack_get_plug_path('plugindirs') 1983 local rev = git_get_hash('v0.0.1', 'plugindirs') 1984 local res = { active = active, path = path, spec = spec, rev = rev } 1985 if info then 1986 res.branches = { 'main' } 1987 res.tags = { 'v0.0.1' } 1988 end 1989 return res 1990 end 1991 1992 it('returns list with necessary data', function() 1993 local basic_data, defbranch_data, plugindirs_data 1994 1995 -- Should work just after installation 1996 exec_lua(function() 1997 vim.pack.add({ 1998 repos_src.defbranch, 1999 { src = repos_src.basic, version = 'feat-branch' }, 2000 { src = repos_src.plugindirs, version = vim.version.range('*') }, 2001 }) 2002 end) 2003 defbranch_data = make_defbranch_data(true, true) 2004 basic_data = make_basic_data(true, true) 2005 plugindirs_data = make_plugindirs_data(true, true) 2006 -- Should preserve order in which plugins were `vim.pack.add()`ed 2007 eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()')) 2008 2009 -- Should also list non-active plugins 2010 n.clear() 2011 vim_pack_add({ repos_src.defbranch }) 2012 defbranch_data = make_defbranch_data(true, true) 2013 basic_data = make_basic_data(false, true) 2014 plugindirs_data = make_plugindirs_data(false, true) 2015 2016 -- Should first list active, then non-active (including their latest 2017 -- set `version` which is inferred from lockfile) 2018 eq({ defbranch_data, basic_data, plugindirs_data }, exec_lua('return vim.pack.get()')) 2019 2020 -- Should respect `names` for both active and not active plugins 2021 eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" })')) 2022 eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" })')) 2023 eq({ basic_data, defbranch_data }, exec_lua('return vim.pack.get({ "basic", "defbranch" })')) 2024 2025 local bad_get_cmd = 'return vim.pack.get({ "ccc", "basic", "aaa" })' 2026 matches('Plugin `ccc` is not installed', pcall_err(exec_lua, bad_get_cmd)) 2027 2028 -- Should respect `opts.info` 2029 defbranch_data = make_defbranch_data(true, false) 2030 basic_data = make_basic_data(false, false) 2031 plugindirs_data = make_plugindirs_data(false, false) 2032 eq( 2033 { defbranch_data, basic_data, plugindirs_data }, 2034 exec_lua('return vim.pack.get(nil, { info = false })') 2035 ) 2036 eq({ basic_data }, exec_lua('return vim.pack.get({ "basic" }, { info = false })')) 2037 eq({ defbranch_data }, exec_lua('return vim.pack.get({ "defbranch" }, { info = false })')) 2038 end) 2039 2040 it('respects `data` field', function() 2041 vim_pack_add({ 2042 { src = repos_src.basic, version = 'feat-branch', data = { test = 'value' } }, 2043 { src = repos_src.defbranch, data = 'value' }, 2044 }) 2045 local out = exec_lua(function() 2046 local plugs = vim.pack.get() 2047 ---@type table<string,string> 2048 return { basic = plugs[1].spec.data.test, defbranch = plugs[2].spec.data } 2049 end) 2050 eq({ basic = 'value', defbranch = 'value' }, out) 2051 end) 2052 2053 it('works with `del()`', function() 2054 vim_pack_add({ repos_src.defbranch, { src = repos_src.basic, version = 'feat-branch' } }) 2055 2056 exec_lua(function() 2057 _G.get_log = {} 2058 vim.api.nvim_create_autocmd({ 'PackChangedPre', 'PackChanged' }, { 2059 callback = function() 2060 table.insert(_G.get_log, vim.pack.get()) 2061 end, 2062 }) 2063 end) 2064 2065 -- Should not include removed plugins immediately after they are removed, 2066 -- while still returning list without holes 2067 exec_lua('vim.pack.del({ "defbranch" }, { force = true })') 2068 local defbranch_data = make_defbranch_data(true, true) 2069 local basic_data = make_basic_data(true, true) 2070 eq({ { defbranch_data, basic_data }, { basic_data } }, exec_lua('return _G.get_log')) 2071 end) 2072 2073 it('works with out of sync lockfile', function() 2074 vim_pack_add({ repos_src.basic, repos_src.defbranch }) 2075 eq(2, vim.tbl_count(get_lock_tbl().plugins)) 2076 2077 -- Should first autoinstall missing plugin (with confirmation) 2078 vim.fs.rm(pack_get_plug_path('basic'), { force = true, recursive = true }) 2079 n.clear() 2080 mock_confirm(1) 2081 eq(2, exec_lua('return #vim.pack.get()')) 2082 2083 eq(1, exec_lua('return #_G.confirm_log')) 2084 pack_assert_content('basic', 'return "basic main"') 2085 2086 -- Should regenerate absent lockfile (from present plugins) 2087 vim.fs.rm(get_lock_path()) 2088 n.clear() 2089 eq(2, exec_lua('return #vim.pack.get()')) 2090 eq(2, vim.tbl_count(get_lock_tbl().plugins)) 2091 end) 2092 end) 2093 2094 describe('del()', function() 2095 it('works', function() 2096 local basic_spec = { src = repos_src.basic, version = 'feat-branch' } 2097 vim_pack_add({ repos_src.plugindirs, repos_src.defbranch, basic_spec }) 2098 2099 local assert_on_disk = function(installed_map) 2100 local installed = {} 2101 for p_name, is_installed in pairs(installed_map) do 2102 eq(is_installed, pack_exists(p_name)) 2103 if is_installed then 2104 installed[#installed + 1] = p_name 2105 end 2106 end 2107 2108 table.sort(installed) 2109 local locked = vim.tbl_keys(get_lock_tbl().plugins) 2110 table.sort(locked) 2111 eq(installed, locked) 2112 end 2113 2114 assert_on_disk({ basic = true, defbranch = true, plugindirs = true }) 2115 2116 -- By default should delete only non-active plugins, even if 2117 -- there is active one among input plugin names 2118 n.clear() 2119 vim_pack_add({ repos_src.defbranch }) 2120 watch_events({ 'PackChangedPre', 'PackChanged' }) 2121 2122 local err = pcall_err(exec_lua, function() 2123 vim.pack.del({ 'basic', 'defbranch', 'plugindirs' }) 2124 end) 2125 matches('Some plugins are active and were not deleted: defbranch', err) 2126 2127 assert_on_disk({ basic = false, defbranch = true, plugindirs = false }) 2128 2129 local msg = "vim.pack: Removed plugin 'basic'\nvim.pack: Removed plugin 'plugindirs'" 2130 eq(msg, n.exec_capture('messages')) 2131 2132 -- Should trigger relevant events in order as specified in `vim.pack.add()` 2133 local log = exec_lua('return _G.event_log') 2134 local find_event = make_find_packchanged(log) 2135 eq(1, find_event('Pre', 'delete', 'basic', 'feat-branch', false)) 2136 eq(2, find_event('', 'delete', 'basic', 'feat-branch', false)) 2137 eq(3, find_event('Pre', 'delete', 'plugindirs', nil, false)) 2138 eq(4, find_event('', 'delete', 'plugindirs', nil, false)) 2139 eq(4, #log) 2140 2141 -- Should be possible to force delete active plugins 2142 n.exec('messages clear') 2143 exec_lua('_G.event_log = {}') 2144 exec_lua(function() 2145 vim.pack.del({ 'defbranch' }, { force = true }) 2146 end) 2147 2148 assert_on_disk({ basic = false, defbranch = false, plugindirs = false }) 2149 2150 eq("vim.pack: Removed plugin 'defbranch'", n.exec_capture('messages')) 2151 2152 log = exec_lua('return _G.event_log') 2153 find_event = make_find_packchanged(log) 2154 eq(1, find_event('Pre', 'delete', 'defbranch', nil, true)) 2155 eq(2, find_event('', 'delete', 'defbranch', nil, false)) 2156 eq(2, #log) 2157 end) 2158 2159 it('works without prior `add()`', function() 2160 vim_pack_add({ repos_src.basic }) 2161 n.clear() 2162 2163 eq(true, pack_exists('basic')) 2164 exec_lua(function() 2165 vim.pack.del({ 'basic' }) 2166 end) 2167 eq(false, pack_exists('basic')) 2168 eq({ plugins = {} }, get_lock_tbl()) 2169 end) 2170 2171 it('works with out of sync lockfile', function() 2172 vim_pack_add({ repos_src.basic, repos_src.defbranch, repos_src.plugindirs }) 2173 eq(3, vim.tbl_count(get_lock_tbl().plugins)) 2174 2175 -- Should first autoinstall missing plugin (with confirmation) 2176 vim.fs.rm(pack_get_plug_path('basic'), { force = true, recursive = true }) 2177 n.clear() 2178 mock_confirm(1) 2179 exec_lua('vim.pack.del({ "defbranch" })') 2180 2181 eq(1, exec_lua('return #_G.confirm_log')) 2182 eq(true, pack_exists('basic')) 2183 eq(false, pack_exists('defbranch')) 2184 eq(true, pack_exists('plugindirs')) 2185 2186 -- Should regenerate absent lockfile (from present plugins) 2187 vim.fs.rm(get_lock_path()) 2188 n.clear() 2189 exec_lua('vim.pack.del({ "basic" })') 2190 eq(1, exec_lua('return #vim.pack.get()')) 2191 eq({ 'plugindirs' }, vim.tbl_keys(get_lock_tbl().plugins)) 2192 end) 2193 2194 it('validates input', function() 2195 local function assert(err_pat, input) 2196 local function del_input() 2197 vim.pack.del(input) 2198 end 2199 matches(err_pat, pcall_err(exec_lua, del_input)) 2200 end 2201 2202 assert('list', nil) 2203 2204 -- Should first check if every plugin name represents installed plugin 2205 -- If not - stop early before any delete 2206 vim_pack_add({ repos_src.basic }) 2207 2208 assert('Plugin `ccc` is not installed', { 'ccc', 'basic', 'aaa' }) 2209 eq(true, pack_exists('basic')) 2210 2211 -- Empty list is allowed with warning 2212 n.exec('messages clear') 2213 exec_lua(function() 2214 vim.pack.del({}) 2215 end) 2216 eq('vim.pack: Nothing to remove', n.exec_capture('messages')) 2217 end) 2218 end) 2219 end)