mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 15:01:11 +02:00
177 lines
7.8 KiB
Lua
177 lines
7.8 KiB
Lua
-- The cache contract is the shared Lua-side publication boundary. A writer
|
|
-- may stage outputs in any order, but readiness is published only after the
|
|
-- version-specific required set exists.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check = T.check
|
|
local eq = T.eq
|
|
local CacheContract = require("src.import.CacheContract")
|
|
|
|
local fs = { prefix = "initial/", files = {} }
|
|
local writes = {}
|
|
function fs.exists(path)
|
|
return fs.files[fs.prefix .. path] ~= nil
|
|
end
|
|
function fs.read(path)
|
|
return fs.files[fs.prefix .. path]
|
|
end
|
|
function fs.write(path, value)
|
|
writes[#writes + 1] = fs.prefix .. path
|
|
fs.files[fs.prefix .. path] = value
|
|
return true
|
|
end
|
|
function fs.remove(path)
|
|
fs.files[fs.prefix .. path] = nil
|
|
end
|
|
|
|
local required, isOverride = CacheContract.requiredFilesFor("red")
|
|
check(not isOverride, "Red uses the shared required-file list")
|
|
check(#required > 0, "Red has required outputs")
|
|
eq(CacheContract.markerFor("red"),
|
|
CacheContract.FORMAT .. "ea9bcae617fdf159b045185467ae58b2e4a48b9a",
|
|
"marker contains format and Red SHA-1")
|
|
|
|
for index = 1, #required - 1 do
|
|
fs.files["red/" .. required[index]] = true
|
|
end
|
|
local complete, missing = CacheContract.allRequiredFilesExist("red", fs)
|
|
check(not complete, "missing output keeps cache incomplete")
|
|
eq(missing, required[#required], "missing output is reported")
|
|
local published, publishError = CacheContract.publish("red", fs)
|
|
check(not published, "incomplete cache is not published")
|
|
check(publishError ~= nil, "incomplete publication explains the missing output")
|
|
check(fs.files["red/" .. CacheContract.MARKER_PATH] == nil,
|
|
"incomplete cache has no completion marker")
|
|
|
|
-- Publication must remove a stale marker left by an interrupted replacement,
|
|
-- and must restore the caller prefix on both the success and failure paths.
|
|
fs.files["red/" .. CacheContract.MARKER_PATH] = "old-marker"
|
|
local removedMarker = CacheContract.publish("red", fs)
|
|
check(not removedMarker, "incomplete retry is still rejected")
|
|
check(fs.files["red/" .. CacheContract.MARKER_PATH] == nil,
|
|
"incomplete retry removes a stale completion marker")
|
|
eq(fs.prefix, "initial/", "incomplete publication restores the caller prefix")
|
|
|
|
fs.files["red/" .. required[#required]] = true
|
|
fs.prefix = "caller/prefix/"
|
|
fs.files["red/" .. required[#required]] = true
|
|
for _, path in ipairs(required) do fs.files["red/" .. path] = true end
|
|
published, publishError = CacheContract.publish("red", fs)
|
|
check(published, "complete cache is published")
|
|
eq(publishError, nil, "complete publication has no error")
|
|
eq(fs.prefix, "caller/prefix/", "publication restores the caller prefix")
|
|
eq(fs.files["red/" .. CacheContract.MARKER_PATH], CacheContract.markerFor("red"),
|
|
"marker is written under the version prefix")
|
|
local marker = CacheContract.readMarker("red", fs)
|
|
eq(marker, CacheContract.markerFor("red"), "marker reads through the version prefix")
|
|
eq(writes[#writes], "red/" .. CacheContract.MARKER_PATH,
|
|
"the marker is the only publication write and comes last")
|
|
|
|
-- Every supported version gets its own marker and complete cache semantics;
|
|
-- Yellow adds its three outputs, while Gold/Silver replace the Gen 1 set.
|
|
for _, version in ipairs({ "red", "blue", "yellow", "gold", "silver" }) do
|
|
local versionFiles, override = CacheContract.requiredFilesFor(version)
|
|
for _, path in ipairs(versionFiles) do
|
|
fs.files[version .. "/" .. path] = true
|
|
end
|
|
if not override then
|
|
for _, path in ipairs(CacheContract.VERSION_REQUIRED_FILES[version] or {}) do
|
|
fs.files[version .. "/" .. path] = true
|
|
end
|
|
end
|
|
local ready, missing = CacheContract.allRequiredFilesExist(version, fs)
|
|
check(ready, version .. " complete cache is ready (" .. tostring(missing) .. ")")
|
|
local didPublish = CacheContract.publish(version, fs)
|
|
check(didPublish, version .. " complete cache publishes")
|
|
eq(fs.files[version .. "/" .. CacheContract.MARKER_PATH],
|
|
CacheContract.markerFor(version), version .. " marker is version-scoped")
|
|
eq(fs.prefix, "caller/prefix/", version .. " publication restores prefix")
|
|
check(CacheContract.isReady(version, fs), version .. " complete cache is ready")
|
|
end
|
|
|
|
local gold, goldOverride = CacheContract.requiredFilesFor("gold")
|
|
check(goldOverride, "Gold uses a version-specific required set")
|
|
local goldSet = {}
|
|
for _, path in ipairs(gold) do goldSet[path] = true end
|
|
check(goldSet["assets/generated/battle/hud/balls.png"],
|
|
"Gold required set includes trainer HUD art")
|
|
check(not goldSet["assets/generated/trade/game_boy.png"],
|
|
"Gold required set excludes Gen 1 trade art")
|
|
check(goldSet["data/generated/rom_text.lua"],
|
|
"Gold required set includes the Gen 2 engine text table")
|
|
local silver = CacheContract.requiredFilesFor("silver")
|
|
local silverSet = {}
|
|
for _, path in ipairs(silver) do silverSet[path] = true end
|
|
check(silverSet["data/generated/rom_text.lua"],
|
|
"Silver required set includes the Gen 2 engine text table")
|
|
check(not silverSet["assets/generated/trade/game_boy.png"],
|
|
"Silver required set excludes Gen 1 trade art")
|
|
check(CacheContract.VERSION_REQUIRED_FILES.yellow ~= nil,
|
|
"Yellow has version-specific required outputs")
|
|
|
|
-- A throwing adapter must not strand the process in its temporary prefix.
|
|
local throwingFs = { prefix = "before/" }
|
|
function throwingFs.exists() error("probe failed") end
|
|
local probed, probeError = CacheContract.allRequiredFilesExist("blue", throwingFs)
|
|
check(not probed and probeError ~= nil, "filesystem probe errors are returned")
|
|
eq(throwingFs.prefix, "before/", "probe errors restore the caller prefix")
|
|
function throwingFs.write() error("write failed") end
|
|
function throwingFs.remove() end
|
|
for _, path in ipairs(CacheContract.REQUIRED_FILES) do
|
|
throwingFs.files = throwingFs.files or {}
|
|
throwingFs.files["blue/" .. path] = true
|
|
end
|
|
function throwingFs.exists(path)
|
|
return throwingFs.files[throwingFs.prefix .. path] ~= nil
|
|
end
|
|
local wrote = CacheContract.publish("blue", throwingFs)
|
|
check(not wrote, "write errors are returned")
|
|
eq(throwingFs.prefix, "before/", "write errors restore the caller prefix")
|
|
|
|
-- Source-tree readiness must use the same version lists and reject a cache
|
|
-- when LÖVE cannot identify a real source directory.
|
|
local oldLove = love
|
|
love = nil
|
|
check(not CacheContract.sourceTreeHasData("red"),
|
|
"source-tree check is safe without LÖVE")
|
|
local sourceFiles = {}
|
|
love = {
|
|
filesystem = {
|
|
getRealDirectory = function(path) return sourceFiles[path] end,
|
|
getSource = function() return "/source" end,
|
|
getInfo = function(path)
|
|
return sourceFiles[path] and { type = "file" } or nil
|
|
end,
|
|
},
|
|
}
|
|
for _, path in ipairs(CacheContract.REQUIRED_FILES) do sourceFiles[path] = "/source" end
|
|
check(CacheContract.sourceTreeHasData("red"),
|
|
"Red source tree uses the shared required set")
|
|
sourceFiles[CacheContract.REQUIRED_FILES[2]] = "/save"
|
|
check(not CacheContract.sourceTreeHasData("red"),
|
|
"source-tree readiness rejects a cache-overlaid required file")
|
|
sourceFiles = {}
|
|
local goldFiles = CacheContract.requiredFilesFor("gold")
|
|
for _, path in ipairs(goldFiles) do sourceFiles["gold/" .. path] = "/source" end
|
|
check(CacheContract.sourceTreeHasData("gold"),
|
|
"Gold source tree uses its override set")
|
|
love = oldLove
|
|
|
|
-- Both importer completion paths must call the shared publication boundary.
|
|
local importerFile = assert(io.open("src/import/RomImporter.lua", "r"))
|
|
local importerSource = importerFile:read("*a")
|
|
importerFile:close()
|
|
local completionCalls = 0
|
|
for _ in importerSource:gmatch("CacheContract%.publish%(%s*version") do
|
|
completionCalls = completionCalls + 1
|
|
end
|
|
eq(completionCalls, 1,
|
|
"both thread and coroutine paths converge on one publishing helper")
|
|
check(importerSource:find("self:_completeImport%(version, prefix, displayName%)")
|
|
~= nil, "coroutine completion uses the shared helper")
|
|
check(importerSource:find("pcall%(self%._completeImport") ~= nil,
|
|
"thread completion uses the shared helper")
|
|
|
|
T.finish("rom cache contract")
|