mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-28 15:37:04 -04:00
128 lines
5.1 KiB
Lua
128 lines
5.1 KiB
Lua
-- The self-updater's host gate: which builds may hand off to a downloaded
|
|
-- payload. A packaged build updates in place -- fused (AppImage, Flatpak's
|
|
-- game.love) or unpacked (every PortMaster-style port, which runs
|
|
-- `love <dir>` and so reports isFused() false). A dev / source checkout must
|
|
-- not, and an unknown host must fail closed.
|
|
--
|
|
-- This gate is the difference between the PortMaster SBC port applying an
|
|
-- update and silently ignoring it after the launcher already said "Restart to
|
|
-- update", so the unpacked case is pinned here.
|
|
-- luajit tests/engine/update_boot_host_gate.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local Boot = require("src.update.Boot")
|
|
local love_stub = require("tests.love_stub")
|
|
|
|
local VERSION_KEY = "src.core.Version"
|
|
|
|
-- A packaged build stamps a real engine (X.Y.Z); a working tree keeps the
|
|
-- "0.0.0-dev" placeholder, which is what Version.isDev() reports.
|
|
local function withVersion(isDev, fn)
|
|
local saved = package.loaded[VERSION_KEY]
|
|
package.loaded[VERSION_KEY] = { isDev = function() return isDev end }
|
|
local ok, err = pcall(fn)
|
|
package.loaded[VERSION_KEY] = saved
|
|
if not ok then error(err, 0) end
|
|
end
|
|
|
|
-- Stand in for the running LÖVE process: nil (no love at all) or a filesystem
|
|
-- that reports the given fused state.
|
|
local function withLove(fs, fn)
|
|
local saved = _G.love
|
|
_G.love = fs and { filesystem = fs } or nil
|
|
local ok, err = pcall(fn)
|
|
_G.love = saved
|
|
if not ok then error(err, 0) end
|
|
end
|
|
|
|
local function fsWith(isFused)
|
|
if isFused == nil then return {} end
|
|
return { isFused = function() return isFused end }
|
|
end
|
|
|
|
-- no love at all (headless host): never update
|
|
withLove(nil, function()
|
|
eq(Boot.canUpdateInPlace(), false, "no love.filesystem -> no in-place update")
|
|
end)
|
|
|
|
-- fused wins outright: a binary carrying its own game archive is a packaged
|
|
-- build whatever the engine string says
|
|
withLove(fsWith(true), function()
|
|
withVersion(false, function()
|
|
eq(Boot.canUpdateInPlace(), true, "fused packaged build updates in place")
|
|
end)
|
|
withVersion(true, function()
|
|
eq(Boot.canUpdateInPlace(), true, "fused build updates even with a dev engine string")
|
|
end)
|
|
end)
|
|
|
|
-- the fix: an unpacked packaged build (PortMaster SBC / RG34XXSP) is not fused,
|
|
-- but it carries a released engine and its source on disk is never rewritten,
|
|
-- so a downloaded payload can be mounted over it
|
|
withLove(fsWith(false), function()
|
|
withVersion(false, function()
|
|
eq(Boot.canUpdateInPlace(), true,
|
|
"unpacked released build (love <dir>) updates in place")
|
|
end)
|
|
withVersion(true, function()
|
|
eq(Boot.canUpdateInPlace(), false,
|
|
"unpacked dev checkout (engine 0.0.0-dev) never updates")
|
|
end)
|
|
end)
|
|
|
|
-- a filesystem that cannot answer the fused question at all still updates when
|
|
-- the engine proves this is a packaged build
|
|
withLove(fsWith(nil), function()
|
|
withVersion(false, function()
|
|
eq(Boot.canUpdateInPlace(), true,
|
|
"released build without isFused() still updates")
|
|
end)
|
|
end)
|
|
|
|
-- fail closed: an unreadable / malformed Version must not open the gate
|
|
withLove(fsWith(false), function()
|
|
local saved = package.loaded[VERSION_KEY]
|
|
package.loaded[VERSION_KEY] = "not a module"
|
|
eq(Boot.canUpdateInPlace(), false, "non-table Version -> no in-place update")
|
|
package.loaded[VERSION_KEY] = { engine = "1.4.2" }
|
|
eq(Boot.canUpdateInPlace(), false, "Version without isDev -> no in-place update")
|
|
package.loaded[VERSION_KEY] = nil
|
|
eq(Boot.canUpdateInPlace(), false, "unloadable Version -> no in-place update")
|
|
package.loaded[VERSION_KEY] = saved
|
|
end)
|
|
|
|
-- Boot.run is the first line of love.load: in a dev checkout it must return
|
|
-- false immediately, leaving the bundled game to boot as normal
|
|
withLove(love_stub.filesystem, function()
|
|
withVersion(true, function()
|
|
eq(Boot.run(nil), false, "Boot.run is a no-op in a dev checkout")
|
|
end)
|
|
end)
|
|
|
|
-- overlay shadowing protection: when a base package is upgraded, an older or
|
|
-- equal payload in updates/ must never be chosen and must be queued for deletion
|
|
do
|
|
local candidates = {
|
|
{ name = "gen1recomp-0.2.57.love", engine = "0.2.57", minShell = 1, payloadHost = "love" },
|
|
{ name = "gen1recomp-0.3.0.love", engine = "0.3.0", minShell = 1, payloadHost = "love" },
|
|
}
|
|
local chosen, toDelete = Boot.select(candidates, "0.3.0", 3, "love")
|
|
eq(chosen, nil, "newer base build 0.3.0 rejects older or equal overlays")
|
|
eq(#toDelete, 2, "both older (0.2.57) and equal (0.3.0) overlays are marked for deletion")
|
|
|
|
local newerCandidates = {
|
|
{ name = "gen1recomp-0.2.57.love", engine = "0.2.57", minShell = 1, payloadHost = "love" },
|
|
{ name = "gen1recomp-0.3.1.love", engine = "0.3.1", minShell = 1, payloadHost = "love" },
|
|
}
|
|
local chosenNewer, toDeleteNewer = Boot.select(newerCandidates, "0.3.0", 3, "love")
|
|
eq(chosenNewer, "gen1recomp-0.3.1.love", "strictly newer overlay 0.3.1 is selected")
|
|
eq(#toDeleteNewer, 1, "older overlay 0.2.57 is marked for deletion when 0.3.1 is chosen")
|
|
eq(toDeleteNewer[1], "gen1recomp-0.2.57.love", "older overlay is the victim")
|
|
end
|
|
|
|
T.finish("update_boot_host_gate")
|