local t = require('tests.harness') local fs = require('arti-pack.fs') local installer = require('arti-pack.installer') local Lock = require('arti-pack.lock') local spec_mod = require('arti-pack.spec') 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 -- Build a tag archive that looks like the real thing: everything wrapped in a -- single "-" directory. 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') vim.fn.mkdir(work .. '/' .. root .. '/doc', 'p') write(work .. '/' .. root .. '/lua/marker.lua', 'return "' .. version .. '"') write(work .. '/' .. root .. '/doc/thing.txt', '*thing.txt*\tA help tag\n') local archive = string.format('%s/%s.%s', tmpdir(), root, ext) local cmd if ext == 'zip' then cmd = { 'zip', '-q', '-r', archive, root } else cmd = { 'tar', '-czf', archive, root } end local out = vim.system(cmd, { cwd = work, text = true }):wait() assert(out.code == 0, 'failed to build fixture archive: ' .. tostring(out.stderr)) return archive end local function new_ctx() local base = tmpdir() return { registry = 'file://unused', pack_dir = fs.mkdirp(base .. '/pack/arti-pack'), cache_dir = fs.mkdirp(base .. '/cache'), concurrency = 2, timeout = 30, lock = Lock.new(base .. '/lock.json'), } end local function run(ctx, specs) local results installer.run(ctx, specs, function(r) results = r end) assert( vim.wait(30000, function() return results ~= nil end, 20), 'installer.run never called back' ) return results end local function spec_for(source, tag, archive, extra) local entry = vim.tbl_extend('force', { source, tag = tag, url = 'file://' .. archive }, extra or {}) return assert(spec_mod.parse(entry)) end t.describe('installer.run', function() if not have('zip') or not have('unzip') then t.it('SKIPPED: zip/unzip unavailable', function() end) return end t.it('installs a zip into pack/start and strips the wrapper directory', function() local archive = build_archive('zip', 'plenary.nvim', '0.1.4') local ctx = new_ctx() local results = run(ctx, { spec_for('nvim-lua/plenary.nvim', 'v0.1.4', archive) }) t.eq(1, #results) t.eq('installed', results[1].status, results[1].err) local dir = ctx.pack_dir .. '/start/plenary.nvim' t.truthy(fs.exists(dir .. '/lua/marker.lua'), 'plugin files land at the top level') t.falsy(fs.exists(dir .. '/plenary.nvim-0.1.4'), 'wrapper directory is stripped') t.eq('v0.1.4', ctx.lock:get('plenary.nvim').tag) end) t.it('installs into pack/opt when opt is set', function() local archive = build_archive('zip', 'lazy.nvim', '1.0.0') local ctx = new_ctx() local results = run(ctx, { spec_for('folke/lazy.nvim', 'v1.0.0', archive, { opt = true }) }) t.eq('installed', results[1].status, results[1].err) t.truthy(fs.exists(ctx.pack_dir .. '/opt/lazy.nvim/lua/marker.lua')) t.falsy(fs.exists(ctx.pack_dir .. '/start/lazy.nvim')) t.eq(true, ctx.lock:get('lazy.nvim').opt) end) t.it('installs a tar.gz the same way', function() local archive = build_archive('tar.gz', 'thing.nvim', '2.0.0') local ctx = new_ctx() local results = run(ctx, { spec_for('owner/thing.nvim', 'v2.0.0', archive, { ext = 'tar.gz' }) }) t.eq('installed', results[1].status, results[1].err) t.truthy(fs.exists(ctx.pack_dir .. '/start/thing.nvim/lua/marker.lua')) end) t.it('replaces the old tree on a tag change and reports an update', function() local ctx = new_ctx() run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', build_archive('zip', 'thing.nvim', '1.0.0')) }) write(ctx.pack_dir .. '/start/thing.nvim/leftover.txt', 'stale') local results = run(ctx, { spec_for('owner/thing.nvim', 'v2.0.0', build_archive('zip', 'thing.nvim', '2.0.0')) }) t.eq('updated', results[1].status, results[1].err) t.falsy(fs.exists(ctx.pack_dir .. '/start/thing.nvim/leftover.txt'), 'stale files are cleared') t.eq('v2.0.0', ctx.lock:get('thing.nvim').tag) end) t.it('moves a plugin between start and opt without leaving a copy behind', function() local archive = build_archive('zip', 'thing.nvim', '1.0.0') local ctx = new_ctx() run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', archive) }) run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', archive, { opt = true }) }) t.truthy(fs.exists(ctx.pack_dir .. '/opt/thing.nvim')) t.falsy(fs.exists(ctx.pack_dir .. '/start/thing.nvim')) end) t.it('fails one plugin without abandoning the rest of the run', function() local ctx = new_ctx() local results = run(ctx, { spec_for('owner/good', 'v1.0.0', build_archive('zip', 'good', '1.0.0')), spec_for('owner/missing', 'v1.0.0', tmpdir() .. '/does-not-exist.zip'), spec_for('owner/alsogood', 'v1.0.0', build_archive('zip', 'alsogood', '1.0.0')), }) local byname = {} for _, r in ipairs(results) do byname[r.name] = r end t.eq(3, #results) t.eq('installed', byname.good.status) t.eq('installed', byname.alsogood.status) t.eq('failed', byname.missing.status) t.matches('download failed', byname.missing.err) t.falsy(ctx.lock:get('missing'), 'a failed plugin is not recorded') end) t.it('rejects a download that is not an archive and leaves nothing behind', function() local ctx = new_ctx() local bogus = write(tmpdir() .. '/bogus.zip', '404 not found') local results = run(ctx, { spec_for('owner/bogus', 'v1.0.0', bogus) }) t.eq('failed', results[1].status) t.matches('not a zip', results[1].err) t.falsy(fs.exists(ctx.pack_dir .. '/start/bogus')) end) t.it('reuses a cached archive instead of downloading again', function() local ctx = new_ctx() local archive = build_archive('zip', 'cached.nvim', '1.0.0') run(ctx, { spec_for('owner/cached.nvim', 'v1.0.0', archive) }) t.truthy(fs.exists(ctx.cache_dir .. '/cached.nvim-v1.0.0.zip'), 'archive is cached by name and tag') -- Delete the source the url points at: a second run can only succeed from cache. vim.fn.delete(archive) fs.rm_rf(ctx.pack_dir .. '/start/cached.nvim') local results = run(ctx, { spec_for('owner/cached.nvim', 'v1.0.0', archive) }) t.eq('updated', results[1].status, results[1].err) t.truthy(fs.exists(ctx.pack_dir .. '/start/cached.nvim/lua/marker.lua')) end) t.it('installs several plugins concurrently', function() local ctx = new_ctx() local specs = {} for i = 1, 5 do local name = 'plug' .. i table.insert(specs, spec_for('owner/' .. name, 'v1.0.0', build_archive('zip', name, '1.0.0'))) end local results = run(ctx, specs) t.eq(5, #results) for _, r in ipairs(results) do t.eq('installed', r.status, r.err) t.truthy(fs.exists(ctx.pack_dir .. '/start/' .. r.name .. '/lua/marker.lua')) end end) t.it('returns immediately for an empty spec list', function() t.eq({}, run(new_ctx(), {})) end) t.it('leaves no staging directory behind', function() local ctx = new_ctx() run(ctx, { spec_for('owner/thing.nvim', 'v1.0.0', build_archive('zip', 'thing.nvim', '1.0.0')) }) t.eq({}, fs.entries(ctx.pack_dir .. '/.stage')) end) end)