mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 23:11:15 +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")
|
||||
@@ -1,32 +1,34 @@
|
||||
-- sourceTreeHasData must use each version's required-file list. Gold's
|
||||
-- cache has no Gen 1 trade art / pikachu.png; validating it against
|
||||
-- REQUIRED_FILES made a Gold source tree look incomplete forever.
|
||||
-- sourceTreeHasData must use the engine-owned cache contract. Gold's cache has
|
||||
-- no Gen 1 trade art; validating it against the Gen 1 list made a Gold source
|
||||
-- tree look incomplete forever.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check = T.check
|
||||
local CacheContract = require("src.import.CacheContract")
|
||||
|
||||
local f = assert(io.open("src/import/RomImporter.lua", "r"))
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
|
||||
local start = src:find("local function sourceTreeHasData", 1, true)
|
||||
check(start ~= nil, "sourceTreeHasData is defined")
|
||||
local finish = src:find("\nfunction RomImporter.isReady", start, true)
|
||||
check(finish ~= nil, "sourceTreeHasData ends before isReady")
|
||||
local body = src:sub(start, finish)
|
||||
local readyStart = src:find("function RomImporter.isReady", 1, true)
|
||||
check(readyStart ~= nil, "isReady is defined")
|
||||
local readyEnd = src:find("\nfunction RomImporter.syncAndroidShortcuts", readyStart, true)
|
||||
check(readyEnd ~= nil, "isReady ends before the next importer helper")
|
||||
local readyBody = src:sub(readyStart, readyEnd)
|
||||
|
||||
check(body:find("requiredFilesFor", 1, true) ~= nil,
|
||||
"sourceTreeHasData uses requiredFilesFor (Gold override, not Gen 1 only)")
|
||||
check(body:find("ipairs(REQUIRED_FILES)", 1, true) == nil,
|
||||
"sourceTreeHasData does not iterate the Gen 1 REQUIRED_FILES list raw")
|
||||
check(readyBody:find("CacheContract.isReady", 1, true) ~= nil,
|
||||
"isReady delegates source-tree and cache readiness to the contract")
|
||||
check(readyBody:find("ipairs(REQUIRED_FILES)", 1, true) == nil,
|
||||
"isReady does not iterate the Gen 1 REQUIRED_FILES list raw")
|
||||
|
||||
local helperStart = src:find("local function requiredFilesFor", 1, true)
|
||||
check(helperStart ~= nil, "requiredFilesFor helper exists")
|
||||
local helper = src:sub(helperStart, start)
|
||||
check(helper:find("VERSION_REQUIRED_FILES_OVERRIDE", 1, true) ~= nil,
|
||||
"requiredFilesFor consults VERSION_REQUIRED_FILES_OVERRIDE")
|
||||
check(src:find('"assets/generated/battle/hud/balls.png"', 1, true) ~= nil,
|
||||
local required, isOverride = CacheContract.requiredFilesFor("gold")
|
||||
check(isOverride, "Gold uses the override required-file list")
|
||||
local requiredSet = {}
|
||||
for _, path in ipairs(required) do requiredSet[path] = true end
|
||||
check(requiredSet["assets/generated/battle/hud/balls.png"],
|
||||
"Gold caches require the trainer HUD ball sheet")
|
||||
check(not requiredSet["assets/generated/trade/game_boy.png"],
|
||||
"Gold does not inherit the Gen 1 trade-art requirement")
|
||||
|
||||
T.finish()
|
||||
|
||||
@@ -84,15 +84,15 @@ if extractor then
|
||||
end
|
||||
|
||||
-- a cache imported before #750 has none of the art; listing one of the
|
||||
-- files in REQUIRED_FILES is what makes it re-import
|
||||
local importer = readFile("src/import/RomImporter.lua")
|
||||
T.check(importer ~= nil, "src/import/RomImporter.lua is readable")
|
||||
if importer then
|
||||
local required = importer:match("local REQUIRED_FILES = {(.-)\n}")
|
||||
T.check(required ~= nil, "REQUIRED_FILES parses")
|
||||
-- files in the engine-owned cache contract is what makes it re-import
|
||||
local contract = readFile("src/import/CacheContract.lua")
|
||||
T.check(contract ~= nil, "src/import/CacheContract.lua is readable")
|
||||
if contract then
|
||||
local required = contract:match("CacheContract.REQUIRED_FILES = {(.-)\n}")
|
||||
T.check(required ~= nil, "CacheContract.REQUIRED_FILES parses")
|
||||
T.check(required ~= nil and required:find(
|
||||
'"assets/generated/trade/game_boy.png"', 1, true) ~= nil,
|
||||
"REQUIRED_FILES makes pre-#750 caches re-import the trade art")
|
||||
"cache contract makes pre-#750 caches re-import the trade art")
|
||||
end
|
||||
|
||||
T.finish("trade art import")
|
||||
|
||||
Reference in New Issue
Block a user