mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
Centralize ROM cache publication contract
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
-- 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")
|
||||
Reference in New Issue
Block a user