c75a866c02
The shipped default named one private, internet-unreachable host, so an unconfigured install failed against a server it could never reach rather than saying what to set. - drop the registry default and make the option required - reject a non-string, empty, or non-http(s) registry with the option name and how to set it - ask for a registry only when a run has to resolve one, so clean, list, and specs with an explicit url keep working without it
479 lines
14 KiB
Lua
479 lines
14 KiB
Lua
local t = require('tests.harness')
|
|
local fs = require('arti-pack.fs')
|
|
local Lock = require('arti-pack.lock')
|
|
local arti = require('arti-pack')
|
|
|
|
local function tmpdir()
|
|
local dir = vim.fn.tempname()
|
|
vim.fn.mkdir(dir, 'p')
|
|
return dir
|
|
end
|
|
|
|
local function write(path, contents)
|
|
local fd = assert(io.open(path, 'wb'))
|
|
fd:write(contents)
|
|
fd:close()
|
|
return path
|
|
end
|
|
|
|
local function have(cmd)
|
|
return vim.fn.executable(cmd) == 1
|
|
end
|
|
|
|
local env
|
|
|
|
-- Point stdpath at a throwaway tree so each test gets its own pack dir, cache
|
|
-- and lockfile, and collect notifications instead of printing them.
|
|
local function use_env()
|
|
local base = tmpdir()
|
|
env = {
|
|
data = fs.mkdirp(base .. '/data'),
|
|
cache = fs.mkdirp(base .. '/cache'),
|
|
notices = {},
|
|
}
|
|
env.pack_dir = env.data .. '/site/pack/arti-pack'
|
|
env.lock_path = env.data .. '/arti-pack/lock.json'
|
|
return env
|
|
end
|
|
|
|
use_env()
|
|
|
|
local real_stdpath = vim.fn.stdpath
|
|
local real_notify = vim.notify
|
|
|
|
vim.fn.stdpath = function(what)
|
|
if what == 'data' then
|
|
return env.data
|
|
elseif what == 'cache' then
|
|
return env.cache
|
|
end
|
|
return real_stdpath(what)
|
|
end
|
|
|
|
vim.notify = function(msg, level)
|
|
table.insert(env.notices, { msg = msg, level = level })
|
|
end
|
|
|
|
local function notices()
|
|
local out = {}
|
|
for _, n in ipairs(env.notices) do
|
|
table.insert(out, n.msg)
|
|
end
|
|
return table.concat(out, '\n')
|
|
end
|
|
|
|
local function last_notice()
|
|
return env.notices[#env.notices] and env.notices[#env.notices].msg or ''
|
|
end
|
|
|
|
local function lock()
|
|
return Lock.new(env.lock_path):load()
|
|
end
|
|
|
|
local function record(name, tag, opt)
|
|
local l = lock()
|
|
l:set(name, { tag = tag or 'v1', opt = opt == true })
|
|
assert(l:save())
|
|
end
|
|
|
|
local function place_dir(name, sub)
|
|
local dir = fs.mkdirp(env.pack_dir .. '/' .. sub .. '/' .. name)
|
|
write(dir .. '/marker.lua', 'return true')
|
|
return dir
|
|
end
|
|
|
|
local function install_fake(name, sub, tag)
|
|
place_dir(name, sub)
|
|
record(name, tag, sub == 'opt')
|
|
end
|
|
|
|
-- sync finishes asynchronously; its closing notification is the only signal.
|
|
local function is_terminal(msg)
|
|
return msg:find('everything up to date', 1, true) ~= nil
|
|
or msg:find('%d+ installed') ~= nil
|
|
or msg:find('%d+ updated') ~= nil
|
|
or msg:find('%d+ failed') ~= nil
|
|
end
|
|
|
|
local function drain(from)
|
|
assert(
|
|
vim.wait(30000, function()
|
|
for i = from + 1, #env.notices do
|
|
if is_terminal(env.notices[i].msg) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end, 20),
|
|
'sync never finished: ' .. notices()
|
|
)
|
|
end
|
|
|
|
local function sync(opts)
|
|
local before = #env.notices
|
|
arti.sync(opts)
|
|
drain(before)
|
|
end
|
|
|
|
local function build_archive(ext, repo, version)
|
|
local work = tmpdir()
|
|
local root = string.format('%s-%s', repo, version)
|
|
vim.fn.mkdir(work .. '/' .. root .. '/lua', 'p')
|
|
write(work .. '/' .. root .. '/lua/marker.lua', 'return "' .. version .. '"')
|
|
|
|
local archive = string.format('%s/%s.%s', tmpdir(), root, ext)
|
|
local out = vim.system({ 'zip', '-q', '-r', archive, root }, { cwd = work, text = true }):wait()
|
|
assert(out.code == 0, 'failed to build fixture archive: ' .. tostring(out.stderr))
|
|
return archive
|
|
end
|
|
|
|
local function plugin_entry(source, tag, archive, extra)
|
|
return vim.tbl_extend('force', { source, tag = tag, url = 'file://' .. archive }, extra or {})
|
|
end
|
|
|
|
t.describe('arti-pack.setup', function()
|
|
t.it('creates the start and opt pack directories', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false })
|
|
t.truthy(fs.exists(env.pack_dir .. '/start'), 'start directory is created')
|
|
t.truthy(fs.exists(env.pack_dir .. '/opt'), 'opt directory is created')
|
|
end)
|
|
|
|
t.it('keeps the defaults for options the caller omits', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false })
|
|
t.eq(4, arti.config.concurrency)
|
|
t.eq(120, arti.config.timeout)
|
|
end)
|
|
|
|
t.it('ships no registry default', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false })
|
|
t.eq(nil, arti.config.registry, 'registry is required, so nothing is assumed')
|
|
end)
|
|
|
|
t.it('rejects a registry that is not an http(s) url', function()
|
|
for _, bad in ipairs({ '', 'artifacts.example.com/gh', 'ftp://artifacts.example.com', 42, {} }) do
|
|
use_env()
|
|
arti.setup({ auto_sync = false, registry = bad })
|
|
t.matches('is not an http%(s%) URL', last_notice(), 'rejects ' .. vim.inspect(bad))
|
|
t.matches("setup{ registry = 'https://", last_notice(), 'says how to set it')
|
|
end
|
|
end)
|
|
|
|
t.it('applies the options the caller does set', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, registry = 'https://example.test/gh', concurrency = 1 })
|
|
t.eq('https://example.test/gh', arti.config.registry)
|
|
t.eq(1, arti.config.concurrency)
|
|
t.eq(120, arti.config.timeout, 'untouched options keep their default')
|
|
end)
|
|
|
|
t.it('does not splice defaults into the caller plugin list', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/one', tag = 'v1' }, 'owner/two' } })
|
|
t.eq(2, #arti.config.plugins)
|
|
t.eq('owner/one', arti.config.plugins[1][1])
|
|
t.eq('owner/two', arti.config.plugins[2])
|
|
end)
|
|
|
|
t.it('forgets the plugin list of an earlier setup call', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/one', tag = 'v1' } } })
|
|
arti.setup({ auto_sync = false })
|
|
t.eq({}, arti.config.plugins)
|
|
end)
|
|
|
|
t.it('syncs on its own when auto_sync is left on', function()
|
|
use_env()
|
|
local before = #env.notices
|
|
arti.setup({ plugins = {} })
|
|
drain(before)
|
|
t.matches('everything up to date', notices())
|
|
end)
|
|
end)
|
|
|
|
t.describe('arti-pack without a registry', function()
|
|
t.it('refuses to sync a plugin it would have to resolve against the registry', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/thing.nvim', tag = 'v1' } } })
|
|
|
|
arti.sync()
|
|
|
|
t.matches('registry is not configured', last_notice())
|
|
t.matches("setup{ registry = 'https://", last_notice(), 'says how to set it')
|
|
t.falsy(fs.exists(env.pack_dir .. '/start/thing.nvim'), 'nothing is downloaded')
|
|
end)
|
|
|
|
t.it('names the registry option when a command runs before setup', function()
|
|
use_env()
|
|
arti.config.registry = nil
|
|
arti.config.plugins = { { 'owner/thing.nvim', tag = 'v1' } }
|
|
|
|
arti.sync()
|
|
|
|
t.matches('registry is not configured', last_notice())
|
|
end)
|
|
|
|
t.it('still syncs plugins that carry their own url', function()
|
|
if not have('curl') then
|
|
return
|
|
end
|
|
use_env()
|
|
local before = #env.notices
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', tmpdir() .. '/absent.zip') },
|
|
})
|
|
arti.sync()
|
|
drain(before)
|
|
|
|
t.falsy(notices():find('registry', 1, true), 'a url-only run never asks for a registry')
|
|
end)
|
|
|
|
t.it('cleans without one', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = {} })
|
|
install_fake('drop.nvim', 'start')
|
|
|
|
arti.clean()
|
|
|
|
t.matches('removed drop.nvim', last_notice())
|
|
end)
|
|
|
|
t.it('lists without one', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/here.nvim', tag = 'v1' } } })
|
|
install_fake('here.nvim', 'start', 'v1')
|
|
|
|
t.matches('installed$', arti.list()[1])
|
|
end)
|
|
end)
|
|
|
|
t.describe('arti-pack.sync', function()
|
|
if not have('zip') or not have('unzip') then
|
|
t.it('SKIPPED: zip/unzip unavailable', function() end)
|
|
return
|
|
end
|
|
|
|
t.it('installs a configured plugin and records it in the lockfile', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({ auto_sync = false, plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive) } })
|
|
sync()
|
|
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/thing.nvim/lua/marker.lua'), 'plugin is unpacked into pack/start')
|
|
t.eq('v1.0.0', lock():get('thing.nvim').tag)
|
|
end)
|
|
|
|
t.it('installs an opt plugin under pack/opt', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'lazy.nvim', '1.0.0')
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = { plugin_entry('folke/lazy.nvim', 'v1.0.0', archive, { opt = true }) },
|
|
})
|
|
sync()
|
|
|
|
t.truthy(fs.exists(env.pack_dir .. '/opt/lazy.nvim/lua/marker.lua'))
|
|
t.falsy(fs.exists(env.pack_dir .. '/start/lazy.nvim'))
|
|
end)
|
|
|
|
t.it('does no work on a second run', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({ auto_sync = false, plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive) } })
|
|
sync()
|
|
|
|
env.notices = {}
|
|
sync()
|
|
t.matches('everything up to date', last_notice())
|
|
end)
|
|
|
|
t.it('reinstalls a current plugin when forced', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({ auto_sync = false, plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive) } })
|
|
sync()
|
|
fs.rm_rf(env.pack_dir .. '/start/thing.nvim')
|
|
|
|
env.notices = {}
|
|
sync({ force = true })
|
|
t.matches('1 updated', last_notice())
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/thing.nvim/lua/marker.lua'))
|
|
end)
|
|
|
|
t.it('reinstalls a plugin whose directory disappeared', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({ auto_sync = false, plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive) } })
|
|
sync()
|
|
fs.rm_rf(env.pack_dir .. '/start/thing.nvim')
|
|
|
|
sync()
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/thing.nvim/lua/marker.lua'))
|
|
end)
|
|
|
|
t.it('reports a bad spec and still installs the good ones', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive), { 'not a source' } },
|
|
})
|
|
sync()
|
|
|
|
t.matches('plugins%[2%]', notices())
|
|
t.matches('invalid source', notices())
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/thing.nvim/lua/marker.lua'))
|
|
end)
|
|
|
|
t.it('surfaces an install failure instead of recording the plugin', function()
|
|
use_env()
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', tmpdir() .. '/absent.zip') },
|
|
})
|
|
sync()
|
|
|
|
t.matches('1 failed', last_notice())
|
|
t.falsy(lock():get('thing.nvim'), 'a failed install is not locked')
|
|
end)
|
|
|
|
t.it('refuses to start a second sync while one is running', function()
|
|
use_env()
|
|
local archive = build_archive('zip', 'thing.nvim', '1.0.0')
|
|
arti.setup({ auto_sync = false, plugins = { plugin_entry('owner/thing.nvim', 'v1.0.0', archive) } })
|
|
|
|
local before = #env.notices
|
|
arti.sync()
|
|
arti.sync()
|
|
t.matches('a sync is already running', last_notice())
|
|
drain(before)
|
|
end)
|
|
end)
|
|
|
|
t.describe('arti-pack.clean', function()
|
|
t.it('removes the directories and lock entries that are no longer configured', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/keep.nvim', tag = 'v1' } } })
|
|
install_fake('keep.nvim', 'start')
|
|
install_fake('drop.nvim', 'start')
|
|
install_fake('dropopt.nvim', 'opt')
|
|
|
|
arti.clean()
|
|
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/keep.nvim'), 'a configured plugin is kept')
|
|
t.falsy(fs.exists(env.pack_dir .. '/start/drop.nvim'))
|
|
t.falsy(fs.exists(env.pack_dir .. '/opt/dropopt.nvim'))
|
|
t.eq({ 'keep.nvim' }, lock():names())
|
|
t.matches('removed drop.nvim, dropopt.nvim', last_notice())
|
|
end)
|
|
|
|
t.it('does nothing when every install is still configured', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/keep.nvim', tag = 'v1' } } })
|
|
install_fake('keep.nvim', 'start')
|
|
|
|
arti.clean()
|
|
|
|
t.matches('nothing to clean', last_notice())
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/keep.nvim'))
|
|
t.eq({ 'keep.nvim' }, lock():names())
|
|
end)
|
|
|
|
t.it('drops a lock entry whose directory is already gone', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = {} })
|
|
record('ghost.nvim', 'v1')
|
|
|
|
arti.clean()
|
|
|
|
t.eq({}, lock():names())
|
|
end)
|
|
|
|
-- Regression: an unparseable entry used to vanish from the wanted set, so
|
|
-- clean deleted a plugin the user still had configured.
|
|
t.it('deletes nothing while any plugin spec is in error', function()
|
|
use_env()
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = {
|
|
{ 'owner/keep.nvim', tag = 'v1' },
|
|
{ 'owner/broken.nvim' },
|
|
},
|
|
})
|
|
install_fake('keep.nvim', 'start')
|
|
install_fake('broken.nvim', 'start')
|
|
install_fake('orphan.nvim', 'start')
|
|
|
|
arti.clean()
|
|
|
|
t.truthy(
|
|
fs.exists(env.pack_dir .. '/start/broken.nvim/marker.lua'),
|
|
'a still-configured plugin survives its own spec error'
|
|
)
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/keep.nvim'))
|
|
t.truthy(fs.exists(env.pack_dir .. '/start/orphan.nvim'), 'nothing is deleted from a partial spec set')
|
|
t.eq({ 'broken.nvim', 'keep.nvim', 'orphan.nvim' }, lock():names(), 'lock entries are left alone')
|
|
t.matches('missing a tag', notices(), 'the parse error is reported')
|
|
t.matches('clean aborted', last_notice())
|
|
end)
|
|
end)
|
|
|
|
t.describe('arti-pack.list', function()
|
|
local function line_for(lines, needle)
|
|
for _, line in ipairs(lines) do
|
|
if line:find(needle, 1, true) then
|
|
return line
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
t.it('says so when nothing is configured', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false })
|
|
t.eq({ 'no plugins configured' }, arti.list())
|
|
end)
|
|
|
|
t.it('reports the state of every configured and locked plugin', function()
|
|
use_env()
|
|
arti.setup({
|
|
auto_sync = false,
|
|
plugins = {
|
|
{ 'owner/here.nvim', tag = 'v1' },
|
|
{ 'owner/old.nvim', tag = 'v2' },
|
|
{ 'owner/gone.nvim', tag = 'v1' },
|
|
{ 'owner/new.nvim', tag = 'v1' },
|
|
},
|
|
})
|
|
install_fake('here.nvim', 'start', 'v1')
|
|
install_fake('old.nvim', 'start', 'v1')
|
|
record('gone.nvim', 'v1')
|
|
record('orphan.nvim', 'v1')
|
|
|
|
local lines = arti.list()
|
|
|
|
t.matches('installed$', line_for(lines, 'owner/here.nvim'))
|
|
t.matches('outdated %(v1%)$', line_for(lines, 'owner/old.nvim'))
|
|
t.matches('gone from disk$', line_for(lines, 'owner/gone.nvim'))
|
|
t.matches('missing$', line_for(lines, 'owner/new.nvim'))
|
|
t.matches('orphaned$', line_for(lines, 'orphan.nvim'))
|
|
t.eq(5, #lines)
|
|
end)
|
|
|
|
t.it('lists spec errors alongside the plugins', function()
|
|
use_env()
|
|
arti.setup({ auto_sync = false, plugins = { { 'owner/ok.nvim', tag = 'v1' }, 'bogus' } })
|
|
|
|
local lines = arti.list()
|
|
|
|
t.eq(2, #lines)
|
|
t.matches('^error: plugins%[2%]', line_for(lines, 'error:'))
|
|
end)
|
|
end)
|
|
|
|
vim.fn.stdpath = real_stdpath
|
|
vim.notify = real_notify
|