test: cover tests.prefabVaultFieldsSurviveSerialization

This commit is contained in:
dualfroz committed 2026-09-12 15:25:05 +02:00
1 parent 9293cb24dc
commit 8250eacded
1 file changed
+173
+173
View File
@@ -11547,6 +11547,179 @@ function tests.builderDiggingThroughAPrefabVaultsOuterWallOpensARouteToItsSealed
"digging through the outer wall opens a genuine route into the vault's previously sealed interior") "digging through the outer wall opens a genuine route into the vault's previously sealed interior")
end end
function tests.prefabVaultFieldsSurviveSerialization()
local level = arena(21, 11)
level.prefabVaults = { { name = "guardPost", x = 5, y = 4, w = 7, h = 5 } }
local restored = Level.fromData(level:toData())
eq(#restored.prefabVaults, 1, "the prefab vault list survives a round trip")
eq(restored.prefabVaults[1].name, "guardPost", "a placed vault's template name survives a round trip")
eq(restored.prefabVaults[1].x, 5, "a placed vault's anchor x survives a round trip")
eq(restored.prefabVaults[1].w, 7, "a placed vault's footprint width survives a round trip")
end
function tests.saveGameFnv1a32MatchesThePublishedTestVectors()
eq(SaveGame.fnv1a32(""), 2166136261, "the empty string hashes to the FNV-1a32 offset basis")
eq(SaveGame.fnv1a32("a"), 3826002220, "a single byte matches the published FNV-1a32 test vector")
eq(SaveGame.fnv1a32("warren"), SaveGame.fnv1a32("warren"), "hashing the same string twice is deterministic")
ok(SaveGame.fnv1a32("warren") ~= SaveGame.fnv1a32("warreo"), "a one-character change changes the hash")
end
function tests.saveGameEncodeDecodeRoundTripsAValueUnchanged()
local codec = SaveGame.new({})
local data = { hp = 12, name = "vault-keeper", tags = { "brave", "unlucky" } }
local decoded = codec.decode(codec.encode(data))
eq(Serialize.encode(decoded), Serialize.encode(data), "a value survives an encode/decode round trip byte for byte")
end
function tests.saveGameSaveThenLoadRoundTripsThroughARealFile()
local codec = SaveGame.new({})
local path = os.tmpname()
codec.save(path, { hp = 12, tags = { "brave", "unlucky" } })
local data = codec.load(path)
os.remove(path)
eq(data.hp, 12, "a saved number survives a real file round trip")
eq(data.tags[1], "brave", "a saved array survives a real file round trip")
eq(data.tags[2], "unlucky", "array order survives a real file round trip")
end
function tests.saveGameLoadReportsMissingRatherThanErroringOnANonexistentFile()
local codec = SaveGame.new({})
local data, err = codec.load("/tmp/warren_savegame_does_not_exist_998877.sav")
eq(data, nil, "a missing file yields no data")
eq(err, "missing", "the reason is reported as missing, distinct from corruption")
end
function tests.saveGameDecodeRejectsTextThatIsNotValidLuaAtAll()
local codec = SaveGame.new({})
local data, err = codec.decode("{ this is not } valid ( lua at all")
eq(data, nil, "unparseable text yields no data")
ok(contains(err or "", "corrupt"), "the failure is reported as corruption")
end
function tests.saveGameDecodeDetectsATamperedPayloadViaChecksumMismatch()
local codec = SaveGame.new({})
local text = codec.encode({ marker = "TAMPERMARKER", count = 3 })
local tampered = text:gsub("TAMPERMARKER", "TAMPERMARKED", 1)
ok(tampered ~= text, "the substitution actually changed the encoded text")
local data, err = codec.decode(tampered)
eq(data, nil, "a payload that no longer matches its checksum is rejected")
ok(contains(err or "", "checksum mismatch"), "the rejection names the checksum as the reason")
end
function tests.saveGameDecodeRefusesASaveFromAVersionNewerThanTheCodecSupports()
-- One throwaway migration just to make CURRENT_VERSION 2 for this check.
local codec = SaveGame.new({ function(d) return d end })
local payload = Serialize.encode({ x = 1 })
local envelope = {
magic = "WARRENSAVE", version = codec.CURRENT_VERSION + 1,
checksum = SaveGame.fnv1a32(payload), payload = payload,
}
local data, err = codec.decode(Serialize.encode(envelope))
eq(data, nil, "a save newer than this codec understands is refused rather than guessed at")
ok(contains(err or "", "future version"), "the refusal names the version mismatch as the reason")
end
function tests.saveGameWithNoMigrationHistoryPassesLegacyDataThroughUnchanged()
local codec = SaveGame.new({})
local path = os.tmpname()
Serialize.saveFile(path, { greeting = "hi", count = 7 })
local data, err = codec.load(path)
os.remove(path)
eq(err, nil, "a magic-less legacy file loads without an error")
eq(data.greeting, "hi", "legacy string fields survive untouched")
eq(data.count, 7, "legacy numeric fields survive untouched")
end
function tests.saveGameMigrationChainAppliesEachStepInOrderStartingFromVersionOne()
local codec = SaveGame.new({
[1] = function(d) d.trail = d.trail or {}; table.insert(d.trail, "one"); return d end,
[2] = function(d) d.trail = d.trail or {}; table.insert(d.trail, "two"); return d end,
})
eq(codec.CURRENT_VERSION, 3, "two migrations put the current version one past the last step")
local path = os.tmpname()
Serialize.saveFile(path, { trail = {} })
local data = codec.load(path)
os.remove(path)
eq(#data.trail, 2, "both migration steps ran")
eq(data.trail[1], "one", "step one ran before step two")
eq(data.trail[2], "two", "step two ran after step one")
end
function tests.saveGameMigrationChainSkipsStepsAlreadyPastTheStoredVersion()
local codec = SaveGame.new({
[1] = function(d) d.trail = d.trail or {}; table.insert(d.trail, "one"); return d end,
[2] = function(d) d.trail = d.trail or {}; table.insert(d.trail, "two"); return d end,
})
local payload = Serialize.encode({ trail = {} })
local envelope = {
magic = "WARRENSAVE", version = 2,
checksum = SaveGame.fnv1a32(payload), payload = payload,
}
local data = codec.decode(Serialize.encode(envelope))
eq(#data.trail, 1, "a save already at version 2 only runs the one remaining step")
eq(data.trail[1], "two", "the remaining step is the one from version 2 onward, not version 1's")
end
function tests.metaLoadUpgradesALegacyPreEnvelopeFileWithoutLosingItsRenown()
local path = os.tmpname()
Serialize.saveFile(path, {
renown = 77, totalRuns = 3, wins = 1, bestDepth = 4,
achievements = { boss_slayer = true }, perks = { vigor = true },
})
local meta = Meta.load(path)
os.remove(path)
eq(meta.renown, 77, "renown from a pre-envelope legacy file is not discarded")
eq(meta.achievements.boss_slayer, true, "achievements from a legacy file survive the upgrade")
eq(#meta.graves, 0, "graves defaults in for a file predating the graves feature")
eq(#meta.leaderboard, 0, "leaderboard defaults in for a file predating the leaderboard feature")
eq(#meta.runHistory, 0, "runHistory defaults in for a file predating streaks")
end
function tests.metaLoadStartsFreshWithoutCrashingOnACorruptedFile()
local path = os.tmpname()
local meta = Meta.new()
meta.renown = 500
Meta.save(meta, path)
local file = assert(io.open(path, "r"))
local text = file:read("a")
file:close()
local tampered = text:gsub("500", "999", 1)
local out = assert(io.open(path, "w"))
out:write(tampered)
out:close()
local restored = Meta.load(path)
os.remove(path)
eq(restored.renown, 0, "a checksum-broken meta file starts fresh rather than trusting tampered renown")
end
function tests.gameLoadUpgradesALegacyPreEnvelopeSaveMissingNewerPlayerFields()
local game = Game.new(1, { class = "fighter" })
local data = game:toData()
data.playerExtra.essence = nil
data.playerExtra.skills = nil
data.playerExtra.skillPoints = nil
data.playerExtra.skillCooldowns = nil
data.playerExtra.reputation = nil
data.playerExtra.sanity = nil
data.stats.vaultsOpened = nil
data.stats.setPiecesFound = nil
local path = os.tmpname()
Serialize.saveFile(path, data)
local restored = Game.load(path)
os.remove(path)
eq(restored.player.essence, 0, "essence defaults in for a save predating it")
eq(restored.stats.vaultsOpened, 0, "vaultsOpened defaults in for a save predating it")
eq(restored.stats.setPiecesFound, 0, "setPiecesFound defaults in for a save predating it")
eq(type(restored.player.skills), "table", "skills defaults to an empty table for a save predating the skill tree")
eq(restored.player.skillPoints, 0, "skillPoints defaults in for a save predating the skill tree")
eq(type(restored.player.skillCooldowns), "table",
"skillCooldowns defaults to an empty table for a save predating the skill tree")
eq(type(restored.player.reputation), "table", "reputation defaults to an empty table for a save predating it")
eq(restored.player.sanity, Sanity.MAX, "sanity defaults to full for a save predating the sanity system")
end
function tests.gameLoadRaisesAClearErrorOnATruncatedSaveFileInsteadOfCrashingObscurely()
local game = Game.new(1)
end end
local order = { local order = {