mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
CLOSES #788, CLOSES #795, CLOSES #796, CLOSES #797, CLOSES #805, CLOSES #826, CLOSES #833, CLOSES #835, CLOSES #837, CLOSES #844, CLOSES #845, CLOSES #846, CLOSES #847
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
-- #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")
|
||||
Reference in New Issue
Block a user