mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
9ec3ff26d8
The Windows pickers shell out to PowerShell, which writes the chosen path in the console's OEM codepage (CP437 on en-US): a file named "Pokémon ...zip" came back as Pok\x82mon. The import then failed and the error notice carrying those bytes hard-crashed the mods panel's UTF-8-validating text draw. - All three Windows picker scripts (ROM, mod, save) now force [Console]::OutputEncoding to UTF-8, so returned paths and any notice built from them are valid. - The mod picker also copies the pick to a plain-ASCII temp name and returns that, so a non-ASCII filename actually imports instead of failing the io.open (Windows io.open needs ANSI bytes).
234 lines
9.3 KiB
Lua
234 lines
9.3 KiB
Lua
-- Love-free coverage for the pure halves of src/mods/LauncherMods.lua: the
|
|
-- status derivation (deriveList) over a synthetic manifest list + options
|
|
-- table, and the archive-root location logic (locateRoot). The discovery and
|
|
-- installZip paths need love.filesystem and are exercised by the launcher; the
|
|
-- decision logic under them lives here so a bad range/conflict/root call fails
|
|
-- one line instead of the app.
|
|
-- luajit tests/engine/launcher_mods_tests.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
local Manifest = require("src.mods.Manifest")
|
|
local Version = require("src.core.Version")
|
|
local LauncherMods = require("src.mods.LauncherMods")
|
|
|
|
-- validated manifests are the exact shape deriveList/resolveToggle read
|
|
local function mf(raw)
|
|
return Manifest.validate(raw)
|
|
end
|
|
|
|
-- index a deriveList result by mod id for assertions
|
|
local function byId(list)
|
|
local m = {}
|
|
for _, row in ipairs(list) do m[row.id] = row end
|
|
return m
|
|
end
|
|
|
|
-- ------- badge derivation: category, then profile, then MOD (uppercased)
|
|
|
|
do
|
|
local list = LauncherMods.deriveList({
|
|
mf({ id = "cat", name = "Cat Mod", version = "1.0.0", entry = "m.lua",
|
|
category = "gameplay" }),
|
|
mf({ id = "prof", name = "Prof Mod", version = "1.0.0", entry = "m.lua",
|
|
profile = "overhaul" }),
|
|
mf({ id = "plain", name = "Plain", version = "1.0.0", entry = "m.lua" }),
|
|
}, { mods = {} })
|
|
local m = byId(list)
|
|
eq(m.cat.badge, "GAMEPLAY", "badge uses the manifest category, uppercased")
|
|
eq(m.prof.badge, "OVERHAUL", "badge falls back to the profile when no category")
|
|
-- no category field, so the fallback reaches the profile default ("content")
|
|
eq(m.plain.badge, "CONTENT", "bare manifest badge falls back to the profile")
|
|
eq(#list, 3, "every discovered manifest yields one row")
|
|
check(m.cat.id < m.plain.id and m.plain.id < m.prof.id,
|
|
"rows come back sorted by id (cat < plain < prof)")
|
|
end
|
|
|
|
-- ------- enabled defaults to true; a false entry disables
|
|
|
|
do
|
|
local manifests = {
|
|
mf({ id = "aaa", name = "A", version = "1.0.0", entry = "m.lua" }),
|
|
mf({ id = "bbb", name = "B", version = "1.0.0", entry = "m.lua" }),
|
|
}
|
|
local m = byId(LauncherMods.deriveList(manifests, { mods = { bbb = false } }))
|
|
check(m.aaa.enabled, "a mod with no options entry defaults to enabled")
|
|
check(not m.bbb.enabled, "an explicit false disables the mod")
|
|
eq(m.aaa.status, "ok", "a healthy enabled mod is ok")
|
|
eq(m.aaa.statusDetail, "Ready", "ok detail reads Ready")
|
|
end
|
|
|
|
-- ------- conflict: only when this mod is enabled and the other is too
|
|
|
|
do
|
|
local manifests = {
|
|
mf({ id = "alpha", name = "Alpha", version = "1.0.0", entry = "m.lua",
|
|
conflicts = { "beta" } }),
|
|
mf({ id = "beta", name = "Beta", version = "1.0.0", entry = "m.lua" }),
|
|
}
|
|
-- both enabled: the declaring side (and, symmetrically, the other) conflict
|
|
local both = byId(LauncherMods.deriveList(manifests, { mods = {} }))
|
|
eq(both.alpha.status, "conflict", "enabled mod conflicting with an enabled mod")
|
|
check(both.alpha.statusDetail:find("Beta", 1, true) ~= nil,
|
|
"conflict detail names the other mod")
|
|
eq(both.beta.status, "conflict",
|
|
"resolveToggle conflict is bidirectional: the target is flagged too")
|
|
|
|
-- disable beta: alpha no longer conflicts (nothing enabled to conflict with)
|
|
local off = byId(LauncherMods.deriveList(manifests, { mods = { beta = false } }))
|
|
eq(off.alpha.status, "ok", "no conflict once the other side is disabled")
|
|
eq(off.beta.status, "ok", "a disabled mod is never a conflict")
|
|
end
|
|
|
|
-- ------- warn: unsatisfied game_version range against Version.engine
|
|
|
|
do
|
|
-- a range the -dev engine cannot satisfy (needs a released >=1.0.0)
|
|
local manifests = {
|
|
mf({ id = "future", name = "Future", version = "1.0.0", entry = "m.lua",
|
|
game_version = ">=1.0.0" }),
|
|
}
|
|
local m = byId(LauncherMods.deriveList(manifests, { mods = {} }))
|
|
eq(m.future.status, "warn", "engine outside the game_version range warns")
|
|
check(m.future.statusDetail:find(">=1.0.0", 1, true) ~= nil,
|
|
"version warn detail quotes the required range")
|
|
check(m.future.statusDetail:find(Version.engine, 1, true) ~= nil,
|
|
"version warn detail quotes the engine version")
|
|
end
|
|
|
|
-- ------- warn: hard dependency missing, disabled, or wrong version
|
|
|
|
do
|
|
local base = { id = "base", name = "Base", version = "1.0.0", entry = "m.lua" }
|
|
local needsMissing = { id = "needy", name = "Needy", version = "1.0.0",
|
|
entry = "m.lua", dependencies = { "ghost" } }
|
|
local m = byId(LauncherMods.deriveList({ mf(needsMissing) }, { mods = {} }))
|
|
eq(m.needy.status, "warn", "a missing hard dependency warns")
|
|
check(m.needy.statusDetail:find("not installed", 1, true) ~= nil,
|
|
"missing-dep detail says not installed")
|
|
|
|
-- present but disabled
|
|
local m2 = byId(LauncherMods.deriveList(
|
|
{ mf(base), mf({ id = "needy", name = "Needy", version = "1.0.0",
|
|
entry = "m.lua", dependencies = { "base" } }) },
|
|
{ mods = { base = false } }))
|
|
eq(m2.needy.status, "warn", "a disabled hard dependency warns")
|
|
check(m2.needy.statusDetail:find("disabled", 1, true) ~= nil,
|
|
"disabled-dep detail says disabled")
|
|
|
|
-- present, enabled, but the version is out of range
|
|
local m3 = byId(LauncherMods.deriveList(
|
|
{ mf(base), mf({ id = "needy", name = "Needy", version = "1.0.0",
|
|
entry = "m.lua", dependencies = { "base@>=2.0.0" } }) },
|
|
{ mods = {} }))
|
|
eq(m3.needy.status, "warn", "a dependency below the required range warns")
|
|
eq(m3.base.status, "ok", "the satisfied dependency itself stays ok")
|
|
|
|
-- the same dep satisfied: needy is ok
|
|
local m4 = byId(LauncherMods.deriveList(
|
|
{ mf(base), mf({ id = "needy", name = "Needy", version = "1.0.0",
|
|
entry = "m.lua", dependencies = { "base@>=1.0.0" } }) },
|
|
{ mods = {} }))
|
|
eq(m4.needy.status, "ok", "a satisfied dependency clears the warn")
|
|
end
|
|
|
|
-- ------- conflict outranks warn when a mod trips both
|
|
|
|
do
|
|
local manifests = {
|
|
mf({ id = "alpha", name = "Alpha", version = "1.0.0", entry = "m.lua",
|
|
conflicts = { "beta" }, game_version = ">=1.0.0" }),
|
|
mf({ id = "beta", name = "Beta", version = "1.0.0", entry = "m.lua" }),
|
|
}
|
|
local m = byId(LauncherMods.deriveList(manifests, { mods = {} }))
|
|
eq(m.alpha.status, "conflict",
|
|
"conflict is reported ahead of a version warn on the same mod")
|
|
end
|
|
|
|
-- ------- locateRoot: manifest at the archive root
|
|
|
|
do
|
|
local root, err = LauncherMods.locateRoot({ "manifest.json", "main.lua" })
|
|
eq(root, "", "a root-level manifest.json resolves to the empty prefix")
|
|
eq(err, nil, "no error for a root-level manifest")
|
|
end
|
|
|
|
-- ------- locateRoot: manifest inside a single top-level folder
|
|
|
|
do
|
|
local root = LauncherMods.locateRoot({
|
|
"mymod/manifest.json", "mymod/main.lua", "mymod/assets/x.png" })
|
|
eq(root, "mymod", "a single wrapping folder resolves to that folder name")
|
|
end
|
|
|
|
-- ------- locateRoot: no manifest anywhere
|
|
|
|
do
|
|
local root, err = LauncherMods.locateRoot({ "readme.txt", "stuff/x.lua" })
|
|
eq(root, nil, "an archive with no manifest.json resolves to nil")
|
|
check(err:find("no manifest.json", 1, true) ~= nil,
|
|
"the no-manifest reason is user-presentable")
|
|
end
|
|
|
|
-- ------- locateRoot: multiple top-level folders is ambiguous
|
|
|
|
do
|
|
local root, err = LauncherMods.locateRoot({
|
|
"one/manifest.json", "two/manifest.json" })
|
|
eq(root, nil, "two candidate mod folders resolves to nil")
|
|
check(err:find("single mod folder", 1, true) ~= nil,
|
|
"the ambiguous reason asks for a single mod folder")
|
|
end
|
|
|
|
-- ------- locateRoot: a lone folder without a manifest is not a root
|
|
|
|
do
|
|
local root, err = LauncherMods.locateRoot({ "assets/x.png" })
|
|
eq(root, nil, "a single folder with no manifest is not a mod root")
|
|
check(err ~= nil, "the no-root case carries a reason")
|
|
end
|
|
|
|
-- ------- uninstall: rejects bad ids without needing a real mods tree
|
|
|
|
do
|
|
local ok, err = LauncherMods.uninstall("")
|
|
eq(ok, nil, "empty id is rejected")
|
|
check(tostring(err):find("missing", 1, true) ~= nil, "empty-id reason")
|
|
|
|
ok, err = LauncherMods.uninstall("../escape")
|
|
eq(ok, nil, "path-like ids are rejected")
|
|
check(tostring(err):find("invalid", 1, true) ~= nil, "path-id reason")
|
|
|
|
ok, err = LauncherMods.uninstall("ghost")
|
|
-- Without a mods/ghost tree (and with the love stub's getInfo), uninstall
|
|
-- either needs LOVE or reports not installed -- never silently succeeds.
|
|
eq(ok, nil, "a missing mod does not uninstall")
|
|
check(err ~= nil, "missing-mod uninstall carries a reason")
|
|
end
|
|
|
|
-- ------- issue #325: the Windows pickers must not hand back mangled paths
|
|
|
|
do
|
|
-- PowerShell writes the pick in the console's OEM codepage by default
|
|
-- (Pokémon -> Pok\x82mon), which broke the open AND crashed the mods
|
|
-- panel's UTF-8-validating text draw. Every Windows picker script must
|
|
-- force UTF-8 output, and the mod picker must return an ASCII temp copy
|
|
-- since io.open on Windows needs ANSI bytes to open the file at all.
|
|
local f = assert(io.open("src/import/RomImporter.lua", "rb"))
|
|
local src = f:read("*a")
|
|
f:close()
|
|
local utf8, copies = 0, 0
|
|
for _ in src:gmatch("OutputEncoding=%[Text%.Encoding%]::UTF8") do
|
|
utf8 = utf8 + 1
|
|
end
|
|
check(utf8 >= 3, "all three Windows pickers force UTF-8 output")
|
|
check(src:find("pokeport_mod_pick.zip", 1, true) ~= nil,
|
|
"the mod picker copies the pick to an ASCII temp name")
|
|
check(src:find("Copy%-Item %-LiteralPath") ~= nil,
|
|
"the copy uses the literal picked path")
|
|
end
|
|
|
|
T.finish("launcher_mods")
|