mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
273 lines
11 KiB
Lua
273 lines
11 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
love = love or require("tests.love_stub")
|
|
|
|
local SyncMods = require("src.sync.SyncMods")
|
|
|
|
local function row(id, version, enabled, github)
|
|
return { id = id, version = version, github = github,
|
|
enabledByVersion = enabled }
|
|
end
|
|
|
|
local function deps(installed, indexes, catalog, modOptions)
|
|
local calls = { installed = {}, enabled = {}, indexes = {}, options = {} }
|
|
return calls, {
|
|
modOptions = function() return modOptions or {} end,
|
|
setOptions = function(id, values)
|
|
calls.options[id] = values
|
|
return true
|
|
end,
|
|
installed = function() return installed end,
|
|
indexes = function() return indexes or {} end,
|
|
addIndex = function(url)
|
|
calls.indexes[#calls.indexes + 1] = url
|
|
return { feed = url }
|
|
end,
|
|
findEntry = function(id) return (catalog or {})[id] end,
|
|
install = function(entry)
|
|
calls.installed[#calls.installed + 1] = entry.id
|
|
return true
|
|
end,
|
|
setEnabled = function(id, enabled, version)
|
|
calls.enabled[#calls.enabled + 1] = id .. ":" .. tostring(version)
|
|
return true
|
|
end,
|
|
}
|
|
end
|
|
|
|
do
|
|
local _, d = deps({
|
|
row("zeta", "1.0.0", { red = true, blue = false, yellow = false, gold = false }),
|
|
row("alpha", "2.1.0", { red = true, gold = true }, "someone/alpha"),
|
|
}, { { url = "https://mods.example/index.json",
|
|
feed = "https://mods.example/index.json" } })
|
|
|
|
local manifest = SyncMods.build(d)
|
|
T.eq(manifest.rev, SyncMods.REV, "the manifest carries its shape revision")
|
|
T.eq(#manifest.indexes, 1, "the player's index list rides along")
|
|
T.eq(manifest.indexes[1], "https://mods.example/index.json",
|
|
"as the url they typed")
|
|
T.eq(#manifest.mods, 2, "every installed mod is listed")
|
|
T.eq(manifest.mods[1].id, "alpha", "sorted by id so the manifest is stable")
|
|
T.eq(manifest.mods[1].source, "github:someone/alpha",
|
|
"a github mod records where it came from")
|
|
T.eq(manifest.mods[2].source, "local",
|
|
"a hand-installed mod is marked local rather than invented")
|
|
T.eq(#manifest.mods[1].enabledFor, 2, "alpha is on for two games")
|
|
T.eq(manifest.mods[1].enabledFor[1], "red", "in GameVersion order")
|
|
T.eq(manifest.mods[1].enabledFor[2], "gold", "red then gold")
|
|
T.eq(#manifest.mods[2].enabledFor, 1, "zeta is on for one")
|
|
end
|
|
|
|
do
|
|
local manifest = {
|
|
rev = 1,
|
|
indexes = { "https://mods.example/index.json", "https://other.example/i.json" },
|
|
mods = {
|
|
{ id = "alpha", version = "2.1.0", enabledFor = { "red", "gold" } },
|
|
{ id = "beta", version = "1.0.0", enabledFor = { "red" } },
|
|
{ id = "ghost", version = "0.1.0", source = "local", enabledFor = { "red" } },
|
|
},
|
|
}
|
|
local _, d = deps(
|
|
{ row("alpha", "2.1.0", { red = true }) },
|
|
{ { url = "https://mods.example/index.json",
|
|
feed = "https://mods.example/index.json" } },
|
|
{ beta = { id = "beta" } })
|
|
|
|
local plan = SyncMods.plan(manifest, d)
|
|
T.eq(#plan.indexes, 1, "only the index this device is missing is planned")
|
|
T.eq(plan.indexes[1], "https://other.example/i.json", "the new one")
|
|
T.eq(#plan.toInstall, 1, "one mod can be fetched from an index")
|
|
T.eq(plan.toInstall[1].id, "beta", "the one the catalog knows")
|
|
T.eq(#plan.missing, 1, "the mod nobody publishes is reported, not invented")
|
|
T.eq(plan.missing[1].id, "ghost", "by id")
|
|
T.eq(#plan.toEnable, 2, "every game answer that differs is planned")
|
|
for _, want in ipairs(plan.toEnable) do
|
|
T.check(want.id ~= "ghost",
|
|
"a mod that cannot be installed is never enabled")
|
|
end
|
|
T.eq(SyncMods.planEmpty(plan), false, "a plan with work is not empty")
|
|
|
|
local same = SyncMods.plan({ rev = 1, indexes = {}, mods = {
|
|
{ id = "alpha", version = "2.1.0", enabledFor = { "red" } } } }, d)
|
|
T.eq(SyncMods.planEmpty(same), true, "a matching device plans nothing")
|
|
end
|
|
|
|
do
|
|
local calls, d = deps({}, {}, { beta = { id = "beta" } })
|
|
local plan = {
|
|
indexes = { "https://other.example/i.json" },
|
|
toInstall = { { id = "beta", entry = { id = "beta" } } },
|
|
toEnable = { { id = "beta", version = "red" } },
|
|
missing = { { id = "ghost" } },
|
|
}
|
|
local seen = {}
|
|
local ok = SyncMods.apply(plan, function(done, total, label)
|
|
seen[#seen + 1] = ("%d/%d %s"):format(done, total, label)
|
|
end, d)
|
|
T.eq(ok, true, "applying a plan reports success")
|
|
T.eq(calls.indexes[1], "https://other.example/i.json", "the index is added")
|
|
T.eq(calls.installed[1], "beta", "the mod is installed through the launcher path")
|
|
T.eq(calls.enabled[1], "beta:red", "and enabled for the game that wanted it")
|
|
T.eq(#seen, 3, "progress is reported once per step")
|
|
T.eq(seen[3], "3/3 beta", "counting up to the total")
|
|
end
|
|
|
|
do
|
|
local _, d = deps({}, {}, {})
|
|
d.install = function() return nil, "download failed" end
|
|
local ok, err = SyncMods.apply({
|
|
toInstall = { { id = "beta", entry = { id = "beta" } } } }, nil, d)
|
|
T.eq(ok, false, "a failed install fails the apply")
|
|
T.check(tostring(err):find("download failed", 1, true) ~= nil,
|
|
"naming the mod and the reason")
|
|
end
|
|
|
|
do
|
|
local calls, d = deps({}, {}, {})
|
|
d.install = function() return nil, "download failed" end
|
|
local ok = SyncMods.apply({
|
|
toInstall = { { id = "beta", entry = { id = "beta" } } },
|
|
toEnable = { { id = "beta", version = "red" } },
|
|
}, nil, d)
|
|
T.eq(ok, false, "the apply still reports the failure")
|
|
T.eq(#calls.enabled, 0,
|
|
"a mod whose install failed is not switched on regardless")
|
|
end
|
|
|
|
do
|
|
local calls, d = deps({}, {}, {})
|
|
local steps = SyncMods.steps({
|
|
indexes = { "https://other.example/i.json" },
|
|
toInstall = { { id = "beta", entry = { id = "beta" } } },
|
|
toEnable = { { id = "beta", version = "red" } },
|
|
}, d)
|
|
T.eq(#steps, 3, "a plan splits into one step per unit of work")
|
|
T.eq(steps[1].run(), true, "steps run one at a time")
|
|
T.eq(#calls.indexes, 1, "so the caller can draw between them")
|
|
T.eq(#calls.installed, 0, "without the rest of the plan having run yet")
|
|
end
|
|
|
|
do
|
|
local live = {
|
|
alpha = { speed = 3, name = "ASH", on = true, bad = {} },
|
|
zeta = {},
|
|
}
|
|
local _, d = deps({
|
|
row("alpha", "2.1.0", { red = true }),
|
|
row("zeta", "1.0.0", { red = true }),
|
|
}, {}, {}, live)
|
|
|
|
local plain = SyncMods.build(d)
|
|
T.eq(plain.mods[1].options, nil,
|
|
"a mod list shares no options unless the player asks for it")
|
|
T.eq(plain.hasOptions, nil, "and is not flagged as carrying any")
|
|
|
|
local full = SyncMods.build(d, true)
|
|
T.eq(full.hasOptions, true, "opting in flags the list as carrying options")
|
|
T.eq(full.mods[1].options.speed, 3, "the player's own values ride along")
|
|
T.eq(full.mods[1].options.name, "ASH", "text values too")
|
|
T.eq(full.mods[1].options.on, true, "and toggles")
|
|
T.eq(full.mods[1].options.bad, nil,
|
|
"a nested table is never sent: only scalars cross the wire")
|
|
T.eq(full.mods[2].options, nil, "a mod with nothing set sends no bucket")
|
|
end
|
|
|
|
do
|
|
local wide = {}
|
|
for i = 1, SyncMods.MAX_OPTION_KEYS + 20 do wide["k" .. i] = i end
|
|
wide.huge = string.rep("x", SyncMods.MAX_OPTION_TEXT + 100)
|
|
local _, d = deps({ row("alpha", "1.0.0", { red = true }) }, {}, {},
|
|
{ alpha = wide })
|
|
local manifest = SyncMods.build(d, true)
|
|
local n = 0
|
|
for _ in pairs(manifest.mods[1].options) do n = n + 1 end
|
|
T.eq(n, SyncMods.MAX_OPTION_KEYS, "an option bucket is capped")
|
|
local kept = manifest.mods[1].options.huge
|
|
T.check(kept == nil or #kept == SyncMods.MAX_OPTION_TEXT,
|
|
"and a long string is clamped when it makes the cut")
|
|
end
|
|
|
|
do
|
|
local manifest = { rev = 2, indexes = {}, hasOptions = true, mods = {
|
|
{ id = "alpha", version = "2.1.0", enabledFor = { "red" },
|
|
options = { speed = 1, name = "MISTY" } },
|
|
{ id = "beta", version = "1.0.0", enabledFor = { "red" },
|
|
options = { theme = "dark" } },
|
|
{ id = "same", version = "1.0.0", enabledFor = { "red" },
|
|
options = { pitch = 5 } },
|
|
{ id = "ghost", version = "0.1.0", enabledFor = { "red" },
|
|
options = { anything = 1 } },
|
|
} }
|
|
local calls, d = deps(
|
|
{ row("alpha", "2.1.0", { red = true }), row("same", "1.0.0", { red = true }) },
|
|
{}, { beta = { id = "beta" } },
|
|
{ alpha = { speed = 3, name = "ASH" }, same = { pitch = 5 } })
|
|
|
|
local plan = SyncMods.plan(manifest, d)
|
|
T.eq(#plan.options, 2, "only mods this device can actually run are listed")
|
|
T.eq(plan.options[1].id, "alpha", "the installed one whose values differ")
|
|
T.eq(plan.options[2].id, "beta", "and the one this plan installs")
|
|
for _, row in ipairs(plan.options) do
|
|
T.check(row.id ~= "same", "a mod already set that way is not busywork")
|
|
T.check(row.id ~= "ghost", "and a mod that cannot be installed is skipped")
|
|
end
|
|
T.eq(plan.applyOptions, nil, "nobody's options are imported unasked")
|
|
T.eq(SyncMods.planHasOptions(plan), true, "the plan reports it has some")
|
|
T.eq(SyncMods.optionsAnswered(plan), false, "and that the question is open")
|
|
|
|
local steps = SyncMods.steps(plan, d)
|
|
local labels = 0
|
|
for _, step in ipairs(steps) do
|
|
if step.label == "alpha" then labels = labels + 1 end
|
|
end
|
|
T.eq(labels, 0, "an unanswered plan writes no options")
|
|
|
|
SyncMods.answerOptions(plan, false)
|
|
T.eq(SyncMods.optionsAnswered(plan), true, "declining answers the question")
|
|
SyncMods.apply(plan, nil, d)
|
|
T.eq(next(calls.options), nil, "and keeps the options this device already had")
|
|
|
|
SyncMods.answerOptions(plan, true)
|
|
T.eq(plan.applyOptions, true, "accepting arms the option steps")
|
|
SyncMods.apply(plan, nil, d)
|
|
T.eq(calls.options.alpha.speed, 1, "the sharer's values are written")
|
|
T.eq(calls.options.alpha.name, "MISTY", "every key they set")
|
|
T.eq(calls.options.beta.theme, "dark", "including a mod installed by the plan")
|
|
end
|
|
|
|
do
|
|
local manifest = { rev = 2, indexes = {}, mods = {
|
|
{ id = "alpha", version = "1.0.0", enabledFor = { "red" },
|
|
options = { speed = 1 } } } }
|
|
local calls, d = deps({ row("alpha", "1.0.0", { red = true }) }, {}, {},
|
|
{ alpha = { speed = 3 } })
|
|
local plan = SyncMods.plan(manifest, d)
|
|
T.eq(SyncMods.planEmpty(plan), true,
|
|
"a list that only differs in options plans no mod work")
|
|
SyncMods.answerOptions(plan, true)
|
|
T.eq(SyncMods.planEmpty(plan), false,
|
|
"until the options are accepted, and then there is work to do")
|
|
d.install = function() return nil, "download failed" end
|
|
SyncMods.apply(plan, nil, d)
|
|
T.eq(calls.options.alpha.speed, 1, "which is just the option write")
|
|
end
|
|
|
|
do
|
|
local manifest = { rev = 2, indexes = {}, mods = {
|
|
{ id = "beta", version = "1.0.0", enabledFor = { "red" },
|
|
options = { speed = 1 } } } }
|
|
local calls, d = deps({}, {}, { beta = { id = "beta" } })
|
|
d.install = function() return nil, "download failed" end
|
|
local plan = SyncMods.plan(manifest, d)
|
|
SyncMods.answerOptions(plan, true)
|
|
local ok = SyncMods.apply(plan, nil, d)
|
|
T.eq(ok, false, "a failed install still fails the apply")
|
|
T.eq(next(calls.options), nil,
|
|
"and the options of a mod that never installed are not written")
|
|
end
|
|
|
|
T.finish("sync_mods")
|