mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
114 lines
5.0 KiB
Lua
114 lines
5.0 KiB
Lua
-- #828: launcher settings "reset" on Android and Steam Deck, with nothing in
|
|
-- the log. Every options write is a WHOLE-FILE rewrite out of the caller's
|
|
-- table (src/core/SaveData.lua saveOptions), so a filesystem that reports a
|
|
-- successful write without the bytes surviving -- an external-storage volume
|
|
-- that went away mid-session (conf.lua sets t.externalstorage on Android), a
|
|
-- read-only or full save dir -- is indistinguishable from "the launcher never
|
|
-- saved at all". saveOptions therefore reads the file back and fails loudly.
|
|
--
|
|
-- This suite pins that contract against injected filesystem stubs, the same
|
|
-- { getInfo, read, write, remove } shape tests/engine/save_slots.lua and
|
|
-- tests/engine/save_file_io_tests.lua use. It is ROM-free (T2 engine tier).
|
|
--
|
|
-- What it does NOT do: prove #828 is fixed. The launcher -> options.lua ->
|
|
-- bootGame chain already round-trips correctly on desktop, so the readback is
|
|
-- instrumentation for the two platforms that report the loss, and the real
|
|
-- verification is a platform run (see the issue). What is testable here is
|
|
-- that a silent no-op write is now reported instead of swallowed.
|
|
-- luajit tests/engine/options_write_readback_bug828.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
love = love or require("tests.love_stub")
|
|
|
|
local Logger = require("src.core.Logger")
|
|
local SaveData = require("src.core.SaveData")
|
|
|
|
local OPTIONS = "options.lua"
|
|
|
|
-- An in-memory love.filesystem stub. `mode` decides what write() does with
|
|
-- the bytes AFTER reporting success, which is the whole point of the suite:
|
|
-- "honest" -- stores them (a working save dir)
|
|
-- "drop" -- reports true, stores nothing (the volume vanished)
|
|
-- "truncate" -- reports true, stores a short prefix (a full save dir)
|
|
-- "fail" -- reports false plus an error string (the pre-existing path)
|
|
local function memfs(mode)
|
|
local files = {}
|
|
return {
|
|
files = files,
|
|
write = function(path, content)
|
|
if mode == "fail" then return false, "no space left on device" end
|
|
if mode == "drop" then return true end
|
|
if mode == "truncate" then
|
|
files[path] = tostring(content):sub(1, 16)
|
|
return true
|
|
end
|
|
files[path] = content
|
|
return true
|
|
end,
|
|
read = function(path) return files[path] end,
|
|
remove = function(path) files[path] = nil return true end,
|
|
getInfo = function(path)
|
|
if files[path] ~= nil then return { type = "file" } end
|
|
return nil
|
|
end,
|
|
}
|
|
end
|
|
|
|
-- SaveData.persistFs hands an injected fs straight back only when it differs
|
|
-- from love.filesystem, so the suite never touches the real save directory.
|
|
local function logged(pattern)
|
|
for i = #Logger.history, 1, -1 do
|
|
if Logger.history[i]:find(pattern, 1, true) then return Logger.history[i] end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- ---- the write that lands: unchanged success contract
|
|
|
|
local fs = memfs("honest")
|
|
local saved = SaveData.saveOptions({ battleLayout = "wide" }, fs)
|
|
check(saved ~= nil, "a write that lands returns the merged options table")
|
|
eq(saved and saved.battleLayout, "wide", "the caller's key survives the merge")
|
|
eq(saved and saved.textSpeed ~= nil, true, "defaults are filled in around it")
|
|
check(fs.files[OPTIONS] ~= nil, "options.lua is written to the injected fs")
|
|
|
|
local loaded = SaveData.loadOptions(fs)
|
|
eq(loaded and loaded.battleLayout, "wide",
|
|
"loadOptions reads back what saveOptions wrote (the launcher -> game hop)")
|
|
|
|
-- ---- the write that silently does not land: the #828 failure mode
|
|
|
|
local dropMark = #Logger.history
|
|
local dropped = SaveData.saveOptions({ battleLayout = "wide" }, memfs("drop"))
|
|
eq(dropped, nil, "a write that reports success but stores nothing returns nil")
|
|
check(logged("options save did not land"),
|
|
"the vanished write is logged, so the next Android report can carry it")
|
|
check(#Logger.history > dropMark, "a log line was actually emitted")
|
|
|
|
-- ---- a partial write is just as lost, and just as loud
|
|
|
|
local truncated = SaveData.saveOptions({ battleLayout = "wide" }, memfs("truncate"))
|
|
eq(truncated, nil, "a truncated write is treated as a failed write")
|
|
check(logged("options save did not land"), "the truncated write is logged too")
|
|
|
|
-- ---- the pre-existing honest failure still behaves exactly as before
|
|
|
|
local failMark = #Logger.history
|
|
local failed = SaveData.saveOptions({ battleLayout = "wide" }, memfs("fail"))
|
|
eq(failed, nil, "a write that returns false still returns nil")
|
|
check(logged("options save failed"),
|
|
"the false-return path keeps its own distinct log line")
|
|
check(#Logger.history > failMark, "the false-return path still logs")
|
|
|
|
-- A dropped write must not be reported through the false-return message:
|
|
-- the two are different diagnoses and the platform reports need to tell
|
|
-- them apart.
|
|
local last = Logger.history[#Logger.history]
|
|
check(last and last:find("options save failed", 1, true) ~= nil,
|
|
"the last failure logged is the false-return one, not the readback one")
|
|
|
|
T.finish("options_write_readback_bug828")
|