package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local T = require("tests.harness") local check, eq = T.check, T.eq local ModUpdate = require("src.mods.ModUpdate") local LauncherMods = require("src.mods.LauncherMods") local Platform = require("src.core.Platform") local CartStore = require("src.carts.CartStore") local RomImporter = require("src.import.RomImporter") local RELEASES = { { version = "2.0.0", zip = { url = "https://example.invalid/a.zip" } }, { version = "1.0.0", zip = { url = "https://example.invalid/b.zip" } }, } local FEED_CART = { kind = "cart", id = "wild_green", title = "Wild Green", author = "Ren", version = "0.29.1", base = "red", seal = "sealed", repo = "https://github.com/ren/wild-green", github = "ren/wild-green", update_check = "ok", latest = { version = "0.29.1", tag = "v0.29.1", zip = { name = "wild_green-0.29.1.g1rcart", url = "https://example.invalid/wild_green.g1rcart" } }, } local function feedAt(version) local entry = {} for k, v in pairs(FEED_CART) do entry[k] = v end entry.version = version entry.latest = { version = version, tag = "v" .. version, zip = { name = "wild_green-" .. version .. ".g1rcart", url = "https://example.invalid/wild_green.g1rcart" } } return entry end local oldBeginFetch = ModUpdate.beginFetchReleases local oldBeginZip = ModUpdate.beginDownloadZip local oldPumpZip = ModUpdate.pumpDownloadZip local oldInstall = LauncherMods.installDownloadedZip local oldDeps = LauncherMods.checkDependencies local oldRemote = Platform.canFetchRemote local oldCartList = CartStore.list local oldCartIndex = CartStore.index local oldCartListFor = CartStore.listFor local oldCartInstall = CartStore.install local failId = nil local installs = {} local cartRows = {} local cartInstalls = {} local cartInstallErr = nil ModUpdate.beginFetchReleases = function() return {} end ModUpdate.beginDownloadZip = function() return {} end ModUpdate.pumpDownloadZip = function() love.filesystem.write("install.zip", "CART BYTES") return true, "install.zip" end LauncherMods.installDownloadedZip = function(id, _, version) installs[#installs + 1] = id if id == failId then return nil, "the archive had no manifest" end return true, version end LauncherMods.checkDependencies = function() return { hasIssues = false } end Platform.canFetchRemote = function() return true end CartStore.list = function() return cartRows end CartStore.listFor = function() return {} end CartStore.index = function() local out = {} for _, row in ipairs(cartRows) do out[#out + 1] = { id = row.id, title = row.title, version = row.version } end return out end CartStore.install = function(bytes) cartInstalls[#cartInstalls + 1] = tostring(bytes) if cartInstallErr then return nil, cartInstallErr end return { id = "wild_green", title = "Wild Green", version = "0.29.1", base = "red" } end local function installedCart(version, repo) return { { id = "wild_green", title = "Wild Green", version = version, cart = { id = "wild_green", version = version, repo = repo } } } end local function info(status, best) return { status = status, latest = best and best.version or nil, best = best, releases = RELEASES } end local function launcher(secondStatus, feedCarts) local ri = setmetatable({ mods = { { id = "one", name = "One", version = "1.0.0", github = "a/one" }, { id = "two", name = "Two", version = "1.0.0", github = "a/two" }, { id = "three", name = "Three", version = "1.0.0" }, }, modUpdateInfo = { one = info("available", RELEASES[1]), two = info(secondStatus or "current", secondStatus == "available" and RELEASES[1] or nil), }, activeCart = {}, carts = {}, findLoaded = true, findIndex = { mods = {}, carts = feedCarts or {} }, }, RomImporter) ri._refreshMods = function() end return ri end local function run(ri, frames) for _ = 1, frames or 40 do if not ri._updateAll then break end ri:_pumpUpdateAll() if ri._updateAll and ri._updateAll.stage == "confirm" then ri:_confirmUpdateAll() end ri:_pumpModInstall() ri:_pumpCartInstall() end end do local ri = launcher() local rows = ri:_updateAllRows() eq(#rows, 1, "only a mod whose badge says a release is available is queued") eq(rows[1].id, "one", "the current mod and the one with no github stay out") ri.modCartPlan = function() return "cart1", { pins = {} }, "gold" end eq(#ri:_updateAllRows(), 0, "a cart owns its mod set, so it offers no rows") end do cartRows = installedCart("0.2.0", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) local rows = ri:_updateAllRows() eq(#rows, 2, "an installed cart the index lists ahead of it is swept too") eq(rows[1].kind, "mod", "mods keep their place at the front of the queue") eq(rows[2].kind, "cart", "and the cart rows follow") eq(rows[2].id, "wild_green", "named by the installed cart") eq(rows[2].from, "0.2.0", "from what is on disk") eq(rows[2].to, "0.29.1", "to what the feed lists") cartRows = installedCart("0.2.0", "someoneelse/wild-green") eq(#launcher(nil, { FEED_CART }):_updateAllCartRows(), 0, "a cart of the same id from another repo is never overwritten") cartRows = { { id = "wild_green", title = "Wild Green", version = "0.2.0" } } local noRepo = launcher(nil, { FEED_CART }):_updateAllCartRows() eq(#noRepo, 1, "a manifest that names no repo matches by id, as the Find tab does") eq((noRepo[1] or {}).to, "0.29.1", "and is offered the listed version") cartRows = installedCart("0.29.1", "ren/wild-green") eq(#launcher(nil, { FEED_CART }):_updateAllCartRows(), 0, "a cart already at the listed version is not queued") cartRows = installedCart("1.0.0", "ren/wild-green") eq(#launcher(nil, { FEED_CART }):_updateAllCartRows(), 0, "and one ahead of it is never downgraded") cartRows = {} end do installs, cartInstalls = {}, {} cartRows = installedCart("0.2.0", "someoneelse/wild-green") local ri = launcher(nil, { FEED_CART }) ri.modUpdateInfo = {} ri:pressUpdateAllMods() ri._modInfoFetch = nil run(ri) eq(#cartInstalls, 0, "a cart from another repo is still never overwritten") check(ri.modNotice.text ~= "Everything is up to date.", "but the sweep no longer reports the skip as up to date") eq(#(ri.modNotice.failures or {}), 1, "it names the cart it passed over") check(tostring((ri.modNotice.failures or {})[1]):find( "ren/wild-green", 1, true) ~= nil, "with the repo the feed lists it under") cartRows = {} end do installs, cartInstalls = {}, {} cartRows = installedCart("0.29.1", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) ri.modUpdateInfo = {} local refreshes = {} ri._refreshFindSources = function(self) self.findSources = { { feed = "https://example.invalid/index.json" } } end ri._refreshFind = function(self, force, opts) refreshes[#refreshes + 1] = { force = force, quiet = opts and opts.quiet } self._findFetch = {} end check(ri:pressUpdateAllMods(), "the press starts the queue") ri._modInfoFetch = nil ri:_pumpUpdateAll() eq(#refreshes, 1, "the sweep fetches the cart feed itself") eq((refreshes[1] or {}).force, true, "forced, not served from the day-old cache") eq((refreshes[1] or {}).quiet, true, "and quiet, since the sweep owns the overlay") eq((ri._updateAll or {}).stage, "check", "the queue waits for that fetch") eq(#cartInstalls, 0, "installing nothing against the cached listing") ri._findFetch = nil ri.findIndex = { mods = {}, carts = { feedAt("0.30.0") } } run(ri) eq(#refreshes, 1, "one forced fetch per sweep, never one per row") eq(#cartInstalls, 1, "the release the fresh feed lists is installed") eq(ri.findNotice, nil, "with no FIND notice left behind") eq(ri.modNotice.text, "Updated 1 items.", "and the run is reported") cartRows = {} end do cartInstalls = {} cartRows = { { id = "wild_green", title = "Wild Green", version = "0.2.0", cart = { id = "wild_green", version = "0.2.0" } } } local function sweep() local ri = launcher(nil, { feedAt("0.2.0") }) ri.modUpdateInfo = {} ri._refreshFindSources = function(self) self.findSources = { { feed = "https://example.invalid/index.json" } } end ri._refreshFind = function(self) self._findFetch = {} end ri:pressUpdateAllMods() ri._modInfoFetch = nil ri:_pumpUpdateAll() ri._findFetch = nil ri.findIndex = { mods = {}, carts = { feedAt("0.29.1") } } for _ = 1, 40 do if not ri._updateAll or ri._updateAll.stage == "confirm" then break end ri:_pumpUpdateAll(); ri:_pumpModInstall(); ri:_pumpCartInstall() end return ri end local ri = sweep() eq((ri._updateAll or {}).stage, "confirm", "a launcher-authored cart the fresh feed lists ahead is asked about first") eq(#cartInstalls, 0, "and nothing is installed before the answer") eq((ri._modConfirm or {}).kind, "updateAllRun", "the dialog is on screen") eq(((ri._modConfirm or {}).lines or {})[1], "Update 1 items?", "built from the fresh feed, not the cached one") eq(((ri._modConfirm or {}).lines or {})[2], "Wild Green", "naming the cart that would be replaced") eq(ri._busy, nil, "with the overlay down") ri._modConfirm = nil run(ri) eq(ri._updateAll, nil, "declining ends the sweep") eq(#cartInstalls, 0, "with the cart left alone") eq(ri.modNotice, nil, "and no notice for a run that never started") ri = sweep() check(ri:_confirmUpdateAll(), "accepting the dialog") run(ri) eq(#cartInstalls, 1, "installs the listed release") eq(ri.modNotice.text, "Updated 1 items.", "and reports it") cartRows = {} end do cartInstalls = {} cartRows = installedCart("0.2.0", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) ri.modUpdateInfo = {} ri._refreshFindSources = function(self) self.findSources = { { feed = "https://example.invalid/index.json" } } end ri._refreshFind = function(self) self._findFetch = {} end ri:pressUpdateAllMods() ri._modInfoFetch = nil ri:_pumpUpdateAll() check(ri._findFetch ~= nil, "the feed fetch is in flight") check(type(ri._busy.cancel) == "function", "behind a cancellable overlay") ri:_cancelUpdateAll() eq(ri._updateAll, nil, "cancelling there ends the sweep on the spot") eq(#cartInstalls, 0, "with nothing installed") check(ri._findFetch ~= nil, "and the fetch left to finish on its own") check(ri.modNotice.text:find("Stopped after updating", 1, true) ~= nil, "and says how far it got") cartRows = {} end do installs, cartInstalls = {}, {} cartRows = installedCart("0.2.0", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) check(ri:pressUpdateAllMods(), "the press starts the queue") ri._modInfoFetch = nil ri._findFetch = {} ri:_pumpUpdateAll() eq(ri._updateAll.stage, "check", "the queue waits for the index feed too") eq(#cartInstalls, 0, "and installs nothing meanwhile") ri._findFetch = nil run(ri) eq(ri._updateAll, nil, "the queue finishes") eq(#installs, 1, "the mod went through the mod installer") eq(installs[1], "one", "by id") eq(#cartInstalls, 1, "and the cart through CartStore.install, not that one") eq(ri.findNotice, nil, "with no FIND notice raised mid-sweep") eq(ri._modConfirm, nil, "and no pin modal to click through") check(ri.modNotice and ri.modNotice.ok, "the run is reported") eq(ri.modNotice.text, "Updated 2 items.", "counting both kinds") end do installs, cartInstalls = {}, {} cartInstallErr = "the cart file was truncated" cartRows = installedCart("0.2.0", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) ri:pressUpdateAllMods() ri._modInfoFetch = nil run(ri) cartInstallErr = nil eq(#installs, 1, "a failed cart does not stop the mod half") eq(ri.modNotice.text, "Updated 1 of 2. 1 failed:", "and is counted") eq(#ri.modNotice.failures, 1, "with one line") check(ri.modNotice.failures[1]:find("Wild Green", 1, true) ~= nil, "naming the cart") cartRows = {} end do cartInstalls = {} cartRows = installedCart("0.2.0", "ren/wild-green") local ri = launcher(nil, { FEED_CART }) local seen = {} ri._updateAll = { stage = "installing" } ri:_beginCartInstall(FEED_CART, { quiet = true, fromUpdateAll = true, done = function(ok) seen[#seen + 1] = ok end }) check(ri._cartInstall ~= nil, "the queue's own cart install is let through") ri:_pumpCartInstall() eq(#cartInstalls, 1, "the bytes reach CartStore.install") eq(ri.findNotice, nil, "a quiet cart install writes no FIND notice") eq(ri._modConfirm, nil, "and raises no pin modal mid-queue") eq(#seen, 1, "the queue is told the row is done") eq(seen[1], true, "and that it worked") cartRows = {} end do installs = {} local ri = launcher("available") check(ri:pressUpdateAllMods(), "the press starts the queue") check(ri._busy ~= nil, "behind the blocking overlay") check(type(ri._busy.cancel) == "function", "which can be cancelled") check(ri._modInfoFetch ~= nil, "the release checks are refreshed first") ri:_pumpUpdateAll() eq(ri._updateAll.stage, "check", "the queue waits for them to drain") eq(#installs, 0, "and installs nothing meanwhile") ri._modInfoFetch = nil ri:_pumpUpdateAll() eq(ri._updateAll.total, 2, "then queues every outdated mod") eq(ri._updateAll.stage, "confirm", "and stops to ask before installing") eq(ri._busy, nil, "with the overlay down so the dialog can be answered") eq((ri._modConfirm or {}).kind, "updateAllRun", "the dialog is up") eq(((ri._modConfirm or {}).lines or {})[1], "Update 2 items?", "counting the rows built after the release checks") eq(((ri._modConfirm or {}).lines or {})[2], "One", "and naming them") ri:_pumpUpdateAll() eq(ri._updateAll.index, 0, "no row starts while the dialog waits") check(ri:_confirmUpdateAll(), "accepting the dialog") eq(ri._modConfirm, nil, "takes it down") check(ri._busy ~= nil, "and puts the overlay back") eq(ri._updateAll.index, 1, "one row is started") ri:_pumpUpdateAll() eq(ri._updateAll.index, 1, "and the next waits for it, not for the frame") eq(#installs, 0, "nothing has unzipped yet") run(ri) eq(ri._updateAll, nil, "the queue finishes") eq(#installs, 2, "having installed both rows") eq(installs[1], "one", "in feed order") eq(installs[2], "two", "one after the other") check(ri.modNotice and ri.modNotice.ok, "and reports the run") eq(ri.modNotice.text, "Updated 2 items.", "as one summary") eq(ri._busy, nil, "with the overlay down") end do installs = {} failId = "two" local ri = launcher("available") ri:pressUpdateAllMods() ri._modInfoFetch = nil run(ri) failId = nil eq(#installs, 2, "the queue carries on past a failure") check(ri.modNotice and not ri.modNotice.ok, "and says the run was partial") eq(ri.modNotice.text, "Updated 1 of 2. 1 failed:", "with the counts") eq(#ri.modNotice.failures, 1, "naming what failed") check(ri.modNotice.failures[1]:find("Two", 1, true) ~= nil, "by the mod's own name") end do local ri = launcher() ri.modUpdateInfo = {} ri:pressUpdateAllMods() ri._modInfoFetch = nil run(ri) eq(ri.modNotice.text, "Everything is up to date.", "a run with no outdated mod or cart says so rather than going quiet") end do Platform.canFetchRemote = function() return false end local ri = launcher("available") local refreshed = false ri._refreshFind = function() refreshed = true end check(ri:pressUpdateAllMods() == false, "no remote fetch, no queue") eq(ri._updateAll, nil, "nothing is started") eq(refreshed, false, "and the cart feed is not fetched either") check(ri.modNotice and not ri.modNotice.ok, "and the refusal is on screen") Platform.canFetchRemote = function() return true end end do installs = {} local ri = launcher("available") ri:pressUpdateAllMods() ri._modInfoFetch = nil ri:_pumpUpdateAll() ri:_confirmUpdateAll() ri:_pumpModInstall() eq(#installs, 1, "one row is through") ri:_cancelUpdateAll() check(ri._updateAll == nil or ri._updateAll.cancelled, "the cancel is taken") run(ri) eq(ri._updateAll, nil, "the queue stops") eq(#installs, 1, "without starting the rest") check(ri.modNotice.text:find("Stopped after updating", 1, true) ~= nil, "and says how far it got") end do local ri = launcher("available") ri:pressUpdateAllMods() ri._modInfoFetch = nil ri:_pumpUpdateAll() ri:_confirmUpdateAll() check(ri._modInstall ~= nil, "a row is downloading") ri:_cancelUpdateAll() check(ri._busy ~= nil, "cancelling mid-download keeps the overlay: the download has no abort") run(ri) eq(ri._busy, nil, "and it comes down once that row is done") end do installs = {} local ri = launcher() ri:_beginModInstall({ modId = "one", name = "One", release = RELEASES[1], verb = "Updated", notice = "mod", quiet = true }) ri:_pumpModInstall() eq(ri.modNotice, nil, "a quiet install writes no per-mod notice") eq(ri._modDepResolver, nil, "and raises no dependency modal mid-queue") end do local ri = launcher("available") ri.cartFillRows = function() return { { id = "one", name = "One", release = RELEASES[1] } } end eq(ri:pressInstallCartMods("red"), true, "a cart fill starts when nothing else is running") ri._cartFill = nil ri:pressUpdateAllMods() check(ri._updateAll ~= nil, "the update-all queue is live") eq(ri:pressInstallCartMods("red"), false, "a cart fill refuses in the frame the queue is between rows") eq(ri._cartFill, nil, "and starts nothing") ri:_beginCartInstall({ id = "cart", title = "Cart", releases = { { version = "1.0.0", zip = { url = "https://x.invalid/c.zip" } } } }) eq(ri._cartInstall, nil, "a cart install refuses there too") eq(ri.findNotice, nil, "silently, since the overlay owns the screen") end do love.graphics.setLineJoin = love.graphics.setLineJoin or function() end love.graphics.newShader = love.graphics.newShader or function() return {} end love.graphics.polygon = love.graphics.polygon or function() end local LauncherView = require("src.import.LauncherView") local Kit = require("src.ui.kit.Kit") local realPrint = love.graphics.print local realButton = Kit.button local buttons, kinds = {}, {} local function draw(imp) local seen = {} buttons, kinds = {}, {} love.graphics.print = function(str, ...) seen[#seen + 1] = tostring(str) return realPrint(str, ...) end Kit.button = function(x, y, w, h, label, opts) buttons[tostring(label)] = not (opts and opts.enabled == false) kinds[tostring(label)] = opts and opts.kind or nil return realButton(x, y, w, h, label, opts) end local ok, err = pcall(LauncherView.draw, imp) love.graphics.print = realPrint Kit.button = realButton check(ok, "the frame draws: " .. tostring(err)) return table.concat(seen, "\n") end local function window(w, h) love.graphics.getDimensions = function() return w, h end love.graphics.getPixelDimensions = function() return w, h end end local imp = RomImporter.new(function() end, { launcher = true }) imp._ensureMods = function() end imp._refreshMods = function() end imp.findLoaded, imp._findFetch = true, nil imp.mods = launcher("available").mods imp.modUpdateInfo = { one = info("available", RELEASES[1]) } imp.tab = "mods" window(1400, 900) local wide = draw(imp) check(wide:find("Update all", 1, true) ~= nil, "a wide header carries Update all beside Check for updates") local cache = imp._modUpdateCountCache eq(cache and cache.count, 1, "the header counts the one outdated mod") draw(imp) check(rawequal(imp._modUpdateCountCache, cache), "and a second frame reuses that count instead of rebuilding a list") imp.modUpdateInfo.two = info("available", RELEASES[1]) imp._modUpdateRev = (imp._modUpdateRev or 0) + 1 draw(imp) eq(imp._modUpdateCountCache.count, 2, "a bumped update revision rebuilds it") window(430, 860) local narrow = draw(imp) check(narrow:find("More...", 1, true) ~= nil, "a phone-width header collapses to More...") imp._modHeaderActionsPopup = true local popup = draw(imp) imp._modHeaderActionsPopup = nil check(popup:find("Update all", 1, true) ~= nil, "and More... is where a phone reaches Update all") window(1400, 900) cartRows = installedCart("0.2.0", "ren/wild-green") imp.mods = {} imp._modUpdateCountCache = nil imp._cartUpdateCache = nil imp._findCartMap = nil imp.findIndex = { mods = {}, carts = { FEED_CART } } local cartsOnly = draw(imp) check(cartsOnly:find("Update all", 1, true) ~= nil, "a launcher with carts but no mods still reaches the sweep") imp.mods = launcher("available").mods imp.modUpdateInfo = { one = info("available", RELEASES[1]) } imp._modUpdateCountCache = nil imp.modScope = "red" imp.activeCart = { red = "wild_green" } imp.modCartPlan = function() return "wild_green", { pins = {}, order = {}, seal = "sealed", title = "Wild Green" }, "red" end local underCart = draw(imp) check(underCart:find("Update all", 1, true) ~= nil, "a selected cart still shows the sweep in the header") eq(buttons["Update all"], true, "and it is live there: replacing a cart with a newer release is legal") eq(buttons["Enable all"], false, "while the bulk pair stays refused, since the cart owns its pins") eq(buttons["Disable all"], false, "on both halves of that pair") local underRows = imp:_updateAllRows() eq(#underRows, 1, "the sweep queues the cart alone") eq(underRows[1].kind, "cart", "and never a pinned mod") eq(imp._modUpdateCountCache.count, 1, "the header count is the sweep's own rows, not the pinned mods' badges") eq(kinds["Update all"], "warn", "and the cart behind tints the button") cartRows = installedCart("0.29.1", "ren/wild-green") imp._modUpdateCountCache = nil imp._cartUpdateCache = nil imp._findCartMap = nil draw(imp) eq(buttons["Update all"], true, "with nothing behind it the button stays live and says so when pressed") eq(kinds["Update all"], "ghost", "untinted: a pinned mod's own badge is not the sweep's business") eq(#imp:_updateAllRows(), 0, "and the sweep has nothing to run") cartRows = installedCart("0.2.0", "ren/wild-green") imp._modUpdateCountCache = nil imp._cartUpdateCache = nil imp._findCartMap = nil window(430, 860) imp._modHeaderActionsPopup = true local cartPopup = draw(imp) imp._modHeaderActionsPopup = nil eq(buttons["Update all"], true, "More... reaches the same live button") eq(buttons["Enable all mods"], false, "with the bulk pair still refused") eq(buttons["Disable all mods"], false, "on both rows") check(cartPopup:find("decides which mods run", 1, true) ~= nil, "and the modal says why those two are grey") window(1400, 900) imp._updateAll = { stage = "check" } draw(imp) eq(buttons["Update all"], false, "a sweep already in flight disables it") imp._updateAll = nil imp.safeMode = true draw(imp) eq(buttons["Update all"], false, "and safe mode disables it too") imp.safeMode = false imp.modCartPlan = nil cartRows = {} end ModUpdate.beginFetchReleases = oldBeginFetch ModUpdate.beginDownloadZip = oldBeginZip ModUpdate.pumpDownloadZip = oldPumpZip LauncherMods.installDownloadedZip = oldInstall LauncherMods.checkDependencies = oldDeps Platform.canFetchRemote = oldRemote CartStore.list = oldCartList CartStore.index = oldCartIndex CartStore.listFor = oldCartListFor CartStore.install = oldCartInstall T.finish("launcher update all")