test: cover tests.prefabVaultFieldsSurviveSerialization
This commit is contained in:
1 parent
9293cb24dc
commit
8250eacded
1 file changed
+173
+173
@@ -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 = {
|
||||||
|
|||||||
Reference in new issue
Block a user