local createLibTest = require('/apis/libtest'); local createCcpm = require('/apis/libccpm'); local testlib = createLibTest({ ... }); local counter = 0; -- Fresh, isolated state + install sandbox per case (never touches real /trapos). local function freshDirs() counter = counter + 1; local stateDir = '/ccpm-test/state-' .. counter; local installRoot = '/ccpm-test/root-' .. counter; fs.delete(stateDir); fs.delete(installRoot); return stateDir, installRoot; end -- Minimal stub of the CC `http` API backed by a url -> body map. local function fakeHttp(routes) return { get = function(url) local body = routes[url]; if not body then return nil; end return { readAll = function() return body; end, close = function() end, }; end, }; end local function ghBase(name, branch) return 'https://raw.githubusercontent.com/' .. name .. '/' .. branch .. '/'; end local function giteaBase(name, branch) return 'https://git.trapcloud.fr/' .. name .. '/raw/branch/' .. branch .. '/'; end testlib.test('registryBaseUrl resolves a github branch', function() local ccpm = createCcpm({ stateDir = freshDirs() }); testlib.assertEquals( ccpm.registryBaseUrl({ name = 'guillaumearm/cc-libs', type = 'github', branch = 'next' }), 'https://raw.githubusercontent.com/guillaumearm/cc-libs/next/' ); end); testlib.test('registryBaseUrl resolves a gitea branch', function() local ccpm = createCcpm({ stateDir = freshDirs() }); testlib.assertEquals( ccpm.registryBaseUrl({ name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' }), 'https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/' ); testlib.assertEquals( ccpm.descriptorUrl({ name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' }, 'trapos-net'), 'https://git.trapcloud.fr/guillaumearm/cc-libs/raw/branch/next/packages/trapos-net/ccpm.json' ); end); testlib.test('registry urls resolve an http base', function() local ccpm = createCcpm({ stateDir = freshDirs() }); testlib.assertEquals( ccpm.registryBaseUrl({ name = 'http://example.com/repo', type = 'http' }), 'http://example.com/repo/' ); testlib.assertEquals( ccpm.descriptorUrl({ name = 'http://example.com/repo/', type = 'http' }, 'trapos-net'), 'http://example.com/repo/packages/trapos-net/ccpm.json' ); end); testlib.test('resolve orders dependencies before dependents', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/a/ccpm.json'] = textutils.serializeJSON({ name = 'a', version = '1', dependencies = { 'b', 'c' }, files = {} }), [base .. 'packages/b/ccpm.json'] = textutils.serializeJSON({ name = 'b', version = '1', dependencies = { 'c' }, files = {} }), [base .. 'packages/c/ccpm.json'] = textutils.serializeJSON({ name = 'c', version = '1', dependencies = {}, files = {} }), }; local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); local ordered, err = ccpm.resolve('a'); testlib.assertTrue(ordered, tostring(err)); testlib.assertEquals(#ordered, 3); testlib.assertEquals(ordered[1].name, 'c'); testlib.assertEquals(ordered[2].name, 'b'); testlib.assertEquals(ordered[3].name, 'a'); end); testlib.test('resolve reports a missing package', function() local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp({}) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); local ordered, err = ccpm.resolve('ghost'); testlib.assertTrue(not ordered); testlib.assertTrue(string.find(err, 'not found', 1, true)); end); testlib.test('resolve detects a dependency cycle', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/a/ccpm.json'] = textutils.serializeJSON({ name = 'a', version = '1', dependencies = { 'b' } }), [base .. 'packages/b/ccpm.json'] = textutils.serializeJSON({ name = 'b', version = '1', dependencies = { 'a' } }), }; local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); local ordered, err = ccpm.resolve('a'); testlib.assertTrue(not ordered); testlib.assertTrue(string.find(err, 'cycle', 1, true)); end); testlib.test('install rejects an already-installed package', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeLock({ packages = { foo = { version = '1', files = {} } } }); local ok, msg = ccpm.install('foo', {}); testlib.assertTrue(not ok); testlib.assertTrue(string.find(msg, 'already installed', 1, true)); testlib.assertTrue(string.find(msg, 'reinstall foo', 1, true)); end); testlib.test('install downloads files and records the lock', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'apis/eventloop.lua' } }), [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' } }), [base .. 'apis/eventloop.lua'] = 'eventloop-body', [base .. 'apis/net.lua'] = 'net-body', }; local sd, root = freshDirs(); local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); local ok = ccpm.install('trapos-net', {}); testlib.assertTrue(ok); testlib.assertTrue(fs.exists(root .. '/apis/net.lua')); testlib.assertTrue(fs.exists(root .. '/apis/eventloop.lua')); local f = fs.open(root .. '/apis/net.lua', 'r'); local body = f.readAll(); f.close(); testlib.assertEquals(body, 'net-body'); local lock = ccpm.readLock(); testlib.assertTrue(lock.packages['trapos-net']); testlib.assertTrue(lock.packages['trapos-core']); testlib.assertEquals(lock.packages['trapos-net'].registry, 'me/repo'); end); testlib.test('install downloads files from a gitea registry', function() local base = giteaBase('guillaumearm/cc-libs', 'next'); local routes = { [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'apis/eventloop.lua' } }), [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' } }), [base .. 'apis/eventloop.lua'] = 'eventloop-body', [base .. 'apis/net.lua'] = 'net-body', }; local sd, root = freshDirs(); local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'guillaumearm/cc-libs', type = 'gitea', branch = 'next' } } }); local ok = ccpm.install('trapos-net', {}); testlib.assertTrue(ok); testlib.assertTrue(fs.exists(root .. '/apis/net.lua')); testlib.assertTrue(fs.exists(root .. '/apis/eventloop.lua')); local f = fs.open(root .. '/apis/net.lua', 'r'); local body = f.readAll(); f.close(); testlib.assertEquals(body, 'net-body'); local lock = ccpm.readLock(); testlib.assertEquals(lock.packages['trapos-net'].registry, 'guillaumearm/cc-libs'); end); testlib.test('installing trapos writes aggregated os state', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/trapos/ccpm.json'] = textutils.serializeJSON({ name = 'trapos', version = '1', dependencies = { 'trapos-net' }, files = {} }), [base .. 'packages/trapos-core/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-core', version = '1', dependencies = {}, files = { 'programs/ccpm.lua' } }), [base .. 'packages/trapos-net/ccpm.json'] = textutils.serializeJSON({ name = 'trapos-net', version = '1', dependencies = { 'trapos-core' }, files = { 'apis/net.lua' }, autostart = { 'servers/ping-server' } }), [base .. 'programs/ccpm.lua'] = 'ccpm-body', [base .. 'apis/net.lua'] = 'net-body', }; local sd, root = freshDirs(); local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); testlib.assertTrue(ccpm.install('trapos', {})); local f = fs.open(sd .. '/manifest.json', 'r'); local manifest = textutils.unserializeJSON(f.readAll()); f.close(); testlib.assertEquals(manifest.version, '1'); testlib.assertEquals(#manifest.files, 2); testlib.assertEquals(manifest.autostart[1], 'servers/ping-server'); end); testlib.test('registry add and remove round-trip', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeConfig({ registries = {} }); testlib.assertTrue(ccpm.addRegistry('foo/bar', { branch = 'next' })); local regs = ccpm.listRegistries(); testlib.assertEquals(#regs, 1); testlib.assertEquals(regs[1].name, 'foo/bar'); testlib.assertEquals(regs[1].branch, 'next'); local dupOk = ccpm.addRegistry('foo/bar', {}); testlib.assertTrue(not dupOk); testlib.assertTrue(ccpm.removeRegistry('foo/bar')); testlib.assertEquals(#ccpm.listRegistries(), 0); local rmOk = ccpm.removeRegistry('nope'); testlib.assertTrue(not rmOk); end); testlib.test('addRegistry defaults to a gitea registry', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeConfig({ registries = {} }); local ok, registry = ccpm.addRegistry('foo/bar', {}); testlib.assertTrue(ok); testlib.assertEquals(registry.type, 'gitea'); testlib.assertEquals(registry.branch, 'master'); local regs = ccpm.listRegistries(); testlib.assertEquals(regs[1].type, 'gitea'); -- The default registry must not silently fall back to github. testlib.assertTrue(regs[1].type ~= 'github'); testlib.assertEquals( ccpm.registryBaseUrl(regs[1]), 'https://git.trapcloud.fr/foo/bar/raw/branch/master/' ); -- An explicit type is still honored (github stays supported but opt-in). testlib.assertTrue(ccpm.addRegistry('legacy/repo', { type = 'github' })); local legacy = ccpm.listRegistries()[2]; testlib.assertEquals(legacy.type, 'github'); end); testlib.test('compareVersions treats padded zeros as equal', function() -- compareVersions is internal; probe via available() status local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeCache({ packages = { pkg = { version = '1.0.0', registry = 'r' } } }); ccpm.writeLock({ packages = { pkg = { version = '1.0', registry = 'r', files = {}, dependencies = {} } } }); local avail = ccpm.available(); testlib.assertEquals(avail[1].status, 'up-to-date'); end); testlib.test('update writes a package cache from registries', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/index.json'] = textutils.serializeJSON({ packages = { foo = '1.0.0', bar = '2.0.0' } }), }; local ccpm = createCcpm({ stateDir = freshDirs(), http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); local cache = ccpm.update(); testlib.assertEquals(cache.packages.foo.version, '1.0.0'); testlib.assertEquals(cache.packages.foo.registry, 'me/repo'); local search = ccpm.search('ba'); testlib.assertEquals(#search, 1); testlib.assertEquals(search[1].name, 'bar'); end); testlib.test('available marks cached packages by install status', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeCache({ packages = { alpha = { version = '1.0.0', registry = 'me/repo' }, beta = { version = '2.0.0', registry = 'me/repo' }, gamma = { version = '1.0.0', registry = 'me/repo' }, } }); ccpm.writeLock({ packages = { alpha = { version = '1.0.0', files = {}, dependencies = {} }, beta = { version = '1.0.0', files = {}, dependencies = {} }, } }); local available = ccpm.available(); testlib.assertEquals(#available, 3); testlib.assertEquals(available[1].name, 'alpha'); testlib.assertEquals(available[1].status, 'up-to-date'); testlib.assertEquals(available[2].name, 'beta'); testlib.assertEquals(available[2].status, 'updatable'); testlib.assertEquals(available[3].name, 'gamma'); testlib.assertEquals(available[3].status, 'available'); end); testlib.test('upgrade reinstalls outdated packages from cache', function() local base = ghBase('me/repo', 'master'); local routes = { [base .. 'packages/foo/ccpm.json'] = textutils.serializeJSON({ name = 'foo', version = '2.0.0', dependencies = {}, files = { 'programs/foo.lua' } }), [base .. 'programs/foo.lua'] = 'foo-v2', }; local sd, root = freshDirs(); local ccpm = createCcpm({ stateDir = sd, installRoot = root, http = fakeHttp(routes) }); ccpm.writeConfig({ registries = { { name = 'me/repo', type = 'github', branch = 'master' } } }); ccpm.writeCache({ packages = { foo = { version = '2.0.0', registry = 'me/repo' } } }); ccpm.writeLock({ packages = { foo = { version = '1.0.0', registry = 'me/repo', files = { 'programs/foo.lua' }, dependencies = {} } } }); local ok, upgraded = ccpm.upgrade({}); testlib.assertTrue(ok); testlib.assertEquals(#upgraded, 1); testlib.assertEquals(upgraded[1], 'foo'); testlib.assertEquals(ccpm.readLock().packages.foo.version, '2.0.0'); local f = fs.open(root .. '/programs/foo.lua', 'r'); local body = f.readAll(); f.close(); testlib.assertEquals(body, 'foo-v2'); end); testlib.test('upgrade requires a package cache', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeLock({ packages = { foo = { version = '1.0.0', files = {}, dependencies = {} } } }); local ok, err = ccpm.upgrade({}); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'ccpm update', 1, true)); end); testlib.test('uninstall refuses a package with dependents', function() local ccpm = createCcpm({ stateDir = freshDirs() }); ccpm.writeLock({ packages = { ['trapos-core'] = { version = '1', files = {}, dependencies = {} }, ['trapos-net'] = { version = '1', files = {}, dependencies = { 'trapos-core' } }, } }); local ok, err = ccpm.uninstall('trapos-core', {}); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'required by', 1, true)); testlib.assertTrue(string.find(err, 'trapos-net', 1, true)); end); testlib.test('uninstall removes a leaf package and its files', function() local sd, root = freshDirs(); fs.makeDir(root .. '/apis'); local f = fs.open(root .. '/apis/net.lua', 'w'); f.write('x'); f.close(); local ccpm = createCcpm({ stateDir = sd, installRoot = root }); ccpm.writeLock({ packages = { ['trapos-core'] = { version = '1', files = {}, dependencies = {} }, ['trapos-net'] = { version = '1', files = { 'apis/net.lua' }, dependencies = { 'trapos-core' } }, } }); testlib.assertTrue(ccpm.uninstall('trapos-net', {})); testlib.assertTrue(not fs.exists(root .. '/apis/net.lua')); testlib.assertTrue(ccpm.readLock().packages['trapos-net'] == nil); testlib.assertTrue(ccpm.readLock().packages['trapos-core'] ~= nil); end); testlib.run();