mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
257 lines
11 KiB
Lua
257 lines
11 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
love = love or require("tests.love_stub")
|
|
|
|
local Manifest = require("src.mods.Manifest")
|
|
local RequiredImports = require("src.mods.RequiredImports")
|
|
local Loader = require("src.mods.Loader")
|
|
local S = require("tests.harness").suite("required mod imports")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local DIGEST = "0123456789abcdef0123456789abcdef"
|
|
local function fakeHash(data)
|
|
return data:sub(1, 4) == "\128\55\18\64" and DIGEST
|
|
or "ffffffffffffffffffffffffffffffff"
|
|
end
|
|
|
|
local manifest = Manifest.validate({
|
|
id = "stadium_fx", name = "Stadium FX", version = "1.0.0", entry = "main.lua",
|
|
required_imports = {
|
|
{ id = "stadium2", name = "Stadium 2", file = "stadium2.z64",
|
|
description = "USA dump", format = "n64", size = 8,
|
|
md5 = { DIGEST, DIGEST:upper() } },
|
|
},
|
|
}, "mods/stadium_fx")
|
|
|
|
eq(#manifest.required_imports, 1, "required import parses")
|
|
eq(#manifest.required_imports[1].md5, 1, "accepted MD5 values normalize and dedupe")
|
|
eq(manifest.required_imports[1].md5[1], DIGEST, "MD5 is lowercase")
|
|
eq(manifest.required_imports[1].description, "USA dump",
|
|
"import description is preserved for the picker UI")
|
|
eq(manifest.required_imports[1].size, 8, "exact import size parses")
|
|
|
|
local optionalManifest = Manifest.validate({
|
|
id = "optional_fx", name = "Optional FX", version = "1.0.0", entry = "main.lua",
|
|
optional_imports = {
|
|
{ id = "bonus", name = "Bonus ROM", file = "bonus.z64",
|
|
format = "n64", md5 = DIGEST },
|
|
},
|
|
}, "mods/optional_fx")
|
|
eq(#optionalManifest.optional_imports, 1, "optional import parses")
|
|
eq(optionalManifest.optional_imports[1].required, false,
|
|
"optional import is marked non-blocking")
|
|
local optionalRows, optionalMissing, missingOptional =
|
|
RequiredImports.inspect(optionalManifest, love.filesystem, fakeHash)
|
|
eq(optionalMissing, 0, "missing optional import is not required")
|
|
eq(missingOptional, 1, "missing optional import is reported separately")
|
|
check(not optionalRows[1].required, "optional row is labeled optional")
|
|
|
|
check(not pcall(Manifest.validate, {
|
|
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
|
required_imports = { { id = "rom", file = "../outside.z64", md5 = DIGEST } },
|
|
}), "required import cannot escape baseroms")
|
|
check(not pcall(Manifest.validate, {
|
|
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
|
required_imports = { { id = "rom", file = "rom.z64", md5 = "short" } },
|
|
}), "malformed MD5 is refused")
|
|
check(not pcall(Manifest.validate, {
|
|
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
|
required_imports = { { id = "rom", file = ".rom.removed", md5 = DIGEST } },
|
|
}), "hidden import filenames cannot collide with engine metadata")
|
|
check(not pcall(Manifest.validate, {
|
|
id = "bad", name = "Bad", version = "1", entry = "main.lua",
|
|
required_imports = { { id = "rom", file = "rom.bin", md5 = DIGEST,
|
|
max_size = RequiredImports.MAX_BYTES + 1 } },
|
|
}), "manifest import sizes cannot exceed the hard limit")
|
|
|
|
local canonical = "\128\55\18\64ABCD"
|
|
local v64 = "\55\128\64\18BADC"
|
|
local n64 = "\64\18\55\128DCBA"
|
|
check(RequiredImports.importData(optionalManifest, "bonus", canonical,
|
|
{ hash = fakeHash }), "optional import uses the normal validation path")
|
|
eq(RequiredImports.normalizeN64(canonical), canonical, "z64 stays canonical")
|
|
eq(RequiredImports.normalizeN64(v64), canonical, "v64 pair swap canonicalizes")
|
|
eq(RequiredImports.normalizeN64(n64), canonical, "n64 word swap canonicalizes")
|
|
eq(RequiredImports.normalizeN64(string.rep("H", 512) .. v64), canonical,
|
|
"recognized 512-byte copier header is stripped")
|
|
check(RequiredImports.normalizeN64(string.rep("H", 520)) == nil,
|
|
"an arbitrary 512-byte prefix is not treated as a copier header")
|
|
|
|
local ok, digest = RequiredImports.importData(manifest, "stadium2", v64,
|
|
{ hash = fakeHash })
|
|
check(ok, "validated bytes import")
|
|
eq(digest, DIGEST, "import reports canonical digest")
|
|
eq(love.filesystem.read("mods/stadium_fx/baseroms/stadium2.z64"), canonical,
|
|
"import writes canonical bytes inside the mod")
|
|
local rows, missing = RequiredImports.inspect(manifest, love.filesystem, fakeHash)
|
|
eq(missing, 0, "written import satisfies its declaration")
|
|
check(rows[1].present, "inspection reports ready")
|
|
|
|
local target = Manifest.validate({
|
|
id = "other_fx", name = "Other FX", version = "1.0.0", entry = "main.lua",
|
|
required_imports = {
|
|
{ id = "same_rom", file = "source.z64", format = "n64", md5 = DIGEST },
|
|
},
|
|
}, "mods/other_fx")
|
|
local targetRows, targetMissing = RequiredImports.inspect(target,
|
|
love.filesystem, fakeHash)
|
|
eq(targetMissing, 1, "matching hashes do not silently share another mod's import")
|
|
check(not targetRows[1].present,
|
|
"a mod needs its own explicit player-selected file")
|
|
check(RequiredImports.remove(target, "same_rom"), "a required import can be removed")
|
|
eq(love.filesystem.read("mods/other_fx/baseroms/source.z64"), nil,
|
|
"remove deletes this mod's private copy")
|
|
check(RequiredImports.importData(target, "same_rom", canonical, { hash = fakeHash }),
|
|
"choosing the file again clears the removal decision")
|
|
|
|
local legacy = Manifest.validate({
|
|
id = "legacy", name = "Legacy", version = "1.0.0", entry = "main.lua",
|
|
}, "mods/legacy")
|
|
local legacyTarget = Manifest.validate({
|
|
id = "legacy_user", name = "Legacy User", version = "1.0.0", entry = "main.lua",
|
|
required_imports = {
|
|
{ id = "rom", file = "legacy-source.z64", format = "n64", md5 = DIGEST },
|
|
},
|
|
}, "mods/legacy_user")
|
|
love.filesystem.write("mods/legacy/baseroms/manually-imported.v64", v64)
|
|
local legacyRows, legacyMissing = RequiredImports.inspect(legacyTarget,
|
|
love.filesystem, fakeHash)
|
|
eq(legacyMissing, 1, "undeclared files in another mod are never indexed")
|
|
check(not legacyRows[1].present, "legacy baseroms remain private to their mod")
|
|
|
|
local capped = Manifest.validate({
|
|
id = "capped", name = "Capped", version = "1.0.0", entry = "main.lua",
|
|
required_imports = {
|
|
{ id = "small", file = "small.bin", md5 = DIGEST, max_size = 4 },
|
|
},
|
|
}, "mods/capped")
|
|
local tooLarge, sizeWhy = RequiredImports.validateData(
|
|
capped.required_imports[1], "12345", fakeHash)
|
|
eq(tooLarge, nil, "per-import size cap rejects before hashing")
|
|
check(tostring(sizeWhy):find("too large", 1, true) ~= nil,
|
|
"size rejection explains the limit")
|
|
|
|
-- A successful validation writes an engine receipt. Matching size + modtime
|
|
-- lets later launcher refreshes avoid reading and hashing the ROM again.
|
|
local cacheFiles = {
|
|
["mods/cache/baseroms/source.z64"] = canonical,
|
|
}
|
|
local dataReads = 0
|
|
local cacheModtime = 123
|
|
local cacheFs = {
|
|
getInfo = function(path, kind)
|
|
local data = cacheFiles[path]
|
|
if data then return { type = "file", size = #data, modtime = cacheModtime } end
|
|
return nil
|
|
end,
|
|
read = function(path)
|
|
if path == "mods/cache/baseroms/source.z64" then dataReads = dataReads + 1 end
|
|
return cacheFiles[path]
|
|
end,
|
|
write = function(path, data) cacheFiles[path] = data return true end,
|
|
remove = function(path) cacheFiles[path] = nil return true end,
|
|
}
|
|
local cacheManifest = Manifest.validate({
|
|
id = "cache", name = "Cache", version = "1.0.0", entry = "main.lua",
|
|
required_imports = {
|
|
{ id = "source", file = "source.z64", format = "n64", size = 8,
|
|
md5 = DIGEST },
|
|
},
|
|
}, "mods/cache")
|
|
local cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, fakeHash)
|
|
check(cacheRows[1].present, "initial cached import validation succeeds")
|
|
cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, function()
|
|
error("unchanged cached import should not be hashed again")
|
|
end)
|
|
check(cacheRows[1].present, "validation receipt satisfies the next refresh")
|
|
eq(dataReads, 1, "unchanged imported ROM is read only once")
|
|
cacheFiles["mods/cache/baseroms/source.z64"] = "BADBYTES"
|
|
cacheModtime = 124
|
|
cacheRows = RequiredImports.inspect(cacheManifest, cacheFs, fakeHash)
|
|
check(not cacheRows[1].present, "changed imported ROM bypasses a stale receipt")
|
|
eq(dataReads, 2, "changed imported ROM is read again")
|
|
eq(cacheFiles[RequiredImports.receiptPath(cacheManifest, cacheManifest.required_imports[1])],
|
|
nil, "stale validation receipt is removed")
|
|
|
|
local rejected, why = RequiredImports.importData(target, "same_rom", "wrong",
|
|
{ hash = fakeHash })
|
|
eq(rejected, nil, "mismatched selection is rejected")
|
|
check(tostring(why):find("N64 ROM (.z64/.v64/.n64)", 1, true) ~= nil,
|
|
"normalization failure explains the selected format")
|
|
|
|
love.filesystem.write("mods/launcher_needs/manifest.json", ([[{
|
|
"id":"launcher_needs","name":"Launcher Needs","version":"1.0.0",
|
|
"entry":"main.lua","required_imports":[{"id":"source","name":"Source ROM",
|
|
"file":"source.bin","md5":"%s"}]
|
|
}]]):format(DIGEST))
|
|
love.filesystem.write("mods/launcher_needs/main.lua", "return function(mod) end")
|
|
local launcherRows = require("src.mods.LauncherMods").list()
|
|
eq(#launcherRows, 1, "launcher keeps a mod with a missing required import visible")
|
|
eq(launcherRows[1].missingRequiredImports, 1,
|
|
"launcher row carries the missing import count")
|
|
eq(launcherRows[1].status, "needs_import",
|
|
"missing import changes Ready to Import required")
|
|
check(launcherRows[1].statusDetail:find("Source ROM", 1, true) ~= nil,
|
|
"launcher warning names the missing file")
|
|
|
|
local function memfs(files)
|
|
return {
|
|
read = function(path) return files[path] end,
|
|
getInfo = function(path)
|
|
if files[path] then return { type = "file" } end
|
|
local prefix = path .. "/"
|
|
for key in pairs(files) do
|
|
if key:sub(1, #prefix) == prefix then return { type = "directory" } end
|
|
end
|
|
end,
|
|
getDirectoryItems = function(path)
|
|
if path == "mods" then
|
|
local out, seen = {}, {}
|
|
for key in pairs(files) do
|
|
local id = key:match("^mods/([^/]+)/manifest%.json$")
|
|
if id and not seen[id] then seen[id] = true; out[#out + 1] = id end
|
|
end
|
|
table.sort(out)
|
|
return out
|
|
end
|
|
return {}
|
|
end,
|
|
load = function(path)
|
|
local source = files[path]
|
|
if not source then return nil, "missing" end
|
|
return load(source, path)
|
|
end,
|
|
}
|
|
end
|
|
|
|
local manifestJson = ([[{
|
|
"id":"needs_rom","name":"Needs ROM","version":"1.0.0","entry":"main.lua",
|
|
"required_imports":[{"id":"rom","name":"Source ROM","file":"source.bin",
|
|
"md5":"%s"}]
|
|
}]]):format(DIGEST)
|
|
local loader = Loader.new({ fs = memfs({
|
|
["mods/needs_rom/manifest.json"] = manifestJson,
|
|
["mods/needs_rom/main.lua"] = "return function(mod) mod.exports.ran = true end",
|
|
}) })
|
|
check(loader:load({}) == false, "missing required import blocks the enabled mod")
|
|
local status = loader:status().available[1]
|
|
eq(status.state, "invalid", "blocked mod reports invalid")
|
|
check(status.error:find("Source ROM", 1, true) ~= nil,
|
|
"loader failure names the required import")
|
|
check(not (loader.exports.needs_rom and loader.exports.needs_rom.ran),
|
|
"blocked mod entry never executes")
|
|
|
|
local optionalJson = ([[{
|
|
"id":"optional_rom","name":"Optional ROM","version":"1.0.0","entry":"main.lua",
|
|
"optional_imports":[{"id":"rom","name":"Bonus ROM","file":"bonus.bin",
|
|
"md5":"%s"}]
|
|
}]]):format(DIGEST)
|
|
local optionalLoader = Loader.new({ fs = memfs({
|
|
["mods/optional_rom/manifest.json"] = optionalJson,
|
|
["mods/optional_rom/main.lua"] = "return function(mod) mod.exports.ran = true end",
|
|
}) })
|
|
check(optionalLoader:load({}), "missing optional import does not block the mod")
|
|
check(optionalLoader.exports.optional_rom.ran,
|
|
"mod entry executes without its optional import")
|
|
|
|
S.finish()
|