Files
gen1recomp/tests/engine/errorhandler_fallback_test.lua
T
2026-09-26 22:35:10 +01:00

260 lines
9.4 KiB
Lua

-- Startup errors still reach LÖVE's native handler if graphics cannot open.
package.path = "./?.lua;./?/init.lua;" .. package.path
_G.love = require("tests.love_stub")
local T = require("tests.harness")
local check, eq = T.check, T.eq
local shown
love.errhand = function(msg)
shown = msg
return "blue handler result"
end
-- Fail the first engine require after main.lua installs the crash handler.
package.preload["src.core.LaunchOptions"] = function()
error("startup probe", 0)
end
package.loaded["src.core.LaunchOptions"] = nil
local loaded, startupError = pcall(dofile, "main.lua")
check(not loaded and tostring(startupError):find("startup probe", 1, true) ~= nil,
"startup probe fails after the crash handler is installed")
check(type(love.errorhandler) == "function", "startup crash handler is installed")
love.filesystem.write("mods/sample_mod/manifest.json",
'{"id":"sample_mod","name":"Sample Mod"}')
eq(love.errorhandler("mods/sample_mod/main.lua:12: crash"),
"blue handler result", "inactive graphics delegates to LÖVE's blue handler")
check(shown:find('Likely source: mod "Sample Mod" (sample_mod)', 1, true) ~= nil,
"blue handler names the mod")
check(shown:find("Error log: /tmp/pokeport-stub-save/lua-error.log", 1, true) ~= nil,
"blue handler shows the exact log path")
check((love.filesystem.read("lua-error.log") or ""):find(
'Likely source: mod "Sample Mod" (sample_mod)', 1, true) ~= nil,
"blue handler saves the attributed error")
eq(love.errorhandler("src/core/Game2.lua:42: crash"),
"blue handler result", "engine errors also use the blue handler")
check(shown:find("Likely source: Game itself (gen1recomp)", 1, true) ~= nil,
"blue handler identifies engine errors")
-- With an active window, the game's own screen keeps the useful summary and
-- log path visible, with the current traceback inside a scrollable panel.
local drawn = {}
local backgrounds = {}
love.graphics.printf = function(value)
drawn[#drawn + 1] = tostring(value)
end
love.graphics.clear = function(r, g, b)
backgrounds[#backgrounds + 1] = { r, g, b }
end
love.graphics.present = function() end
love.graphics.isActive = function() return true end
love.window.isOpen = function() return true end
love.event = {
pump = function() end,
poll = function() return function() return nil end end,
}
love.timer.sleep = function() end
local nativeShown = shown
local drawLoop = love.errorhandler("mods/sample_mod/main.lua:12: raw Lua crash")
eq(type(drawLoop), "function", "active window gets the custom crash screen")
drawLoop()
eq(shown, nativeShown, "custom crash screen does not call the native handler")
local display = table.concat(drawn, "\n")
check(display:find("The game ran into a problem", 1, true) ~= nil,
"screen has a plain-language heading")
check(display:find("Mod: Sample Mod (sample_mod)", 1, true) ~= nil,
"screen names the mod and its id")
check(display:find("Try disabling this mod", 1, true) ~= nil,
"screen gives the player a next step")
check(display:find("/tmp/pokeport-stub-save/lua-error.log", 1, true) ~= nil,
"screen shows the exact log path")
check(display:find("raw Lua crash", 1, true) ~= nil,
"the terminal panel shows the current error details")
check((love.filesystem.read("lua-error.log") or ""):find("raw Lua crash", 1, true) ~= nil,
"technical details remain in the saved log")
local red = backgrounds[#backgrounds]
-- If no window exists yet, open one and render the same layout in blue.
local opened = false
love.window.isOpen = function() return opened end
love.window.setMode = function(width, height)
eq(width, 800, "fallback opens an 800px window")
eq(height, 600, "fallback opens a 600px window")
opened = true
return true
end
drawn, backgrounds = {}, {}
local blueLoop = love.errorhandler("src/core/Game2.lua:42: startup crash")
eq(type(blueLoop), "function", "startup failure gets a custom crash screen")
blueLoop()
display = table.concat(drawn, "\n")
check(display:find("gen1recomp", 1, true) ~= nil,
"blue variant identifies the engine")
check(display:find("Restart the game", 1, true) ~= nil,
"blue variant gives a plain-language next step")
check(display:find("startup crash", 1, true) ~= nil,
"blue variant shows the same technical-details panel")
local blue = backgrounds[#backgrounds]
check(red and blue and red[3] ~= blue[3],
"active and startup crash screens use distinct palettes")
local CrashScreen = require("src.debug.CrashScreen")
drawn = {}
local unsaved = CrashScreen.new({
owner = { kind = "engine", name = "gen1recomp" },
logPath = "/tmp/pokeport-stub-save/lua-error.log",
saved = false,
}, "blue")
CrashScreen.draw(unsaved)
display = table.concat(drawn, "\n")
check(display:find("ERROR LOG NOT SAVED", 1, true) ~= nil,
"screen clearly marks a failed log write")
check(display:find("Take a photo", 1, true) ~= nil,
"failed log write gives an alternative next step")
check(not display:find("Share this file", 1, true),
"failed log write does not tell the player to share a nonexistent file")
check(display:find("Close game", 1, true) ~= nil,
"crash screen has a visible close button")
check(CrashScreen.hitClose(unsaved, 515, 430),
"close button accepts a pointer press")
check(CrashScreen.hitClose(unsaved, 0.81, 0.75),
"close button accepts normalized touch coordinates")
check(not CrashScreen.hitClose(unsaved, 10, 10),
"outside presses do not close the game")
local details = { "first detail line" }
for i = 1, 40 do details[#details + 1] = "trace frame " .. i end
details[#details + 1] = "last detail line"
local longScreen = CrashScreen.new({
owner = { kind = "engine", name = "gen1recomp" },
logPath = "/tmp/pokeport-stub-save/lua-error.log",
details = table.concat(details, "\n"), saved = true,
}, "red")
local clips = {}
local originalScissor = love.graphics.setScissor
love.graphics.setScissor = function(x, y, w, h)
clips[#clips + 1] = { x, y, w, h }
end
drawn = {}
CrashScreen.draw(longScreen)
love.graphics.setScissor = originalScissor
check(longScreen.scrollMax > 0, "long details have a scroll range")
check(#clips >= 2 and clips[1][3] > 0 and clips[1][4] > 0
and clips[#clips][1] == nil,
"details are clipped to the panel and graphics clipping is restored")
check(table.concat(drawn, "\n"):find("first detail line", 1, true) ~= nil,
"the details panel starts at the first line")
CrashScreen.scrollTo(longScreen, true)
drawn = {}
CrashScreen.draw(longScreen)
display = table.concat(drawn, "\n")
check(display:find("last detail line", 1, true) ~= nil
and not display:find("first detail line", 1, true),
"scrolling reaches the final line without drawing hidden lines")
eq(longScreen.scroll, longScreen.scrollMax, "End clamps at the bottom")
CrashScreen.scroll(longScreen, -math.huge)
eq(longScreen.scroll, 0, "scrolling up clamps at the top")
local area = longScreen.detailArea
check(CrashScreen.pointerPressed(longScreen, "touch", area.x + 8, area.y + 25),
"touch can grab the details panel")
CrashScreen.pointerMoved(longScreen, "touch", area.x + 8, area.y + 5)
check(longScreen.scroll > 0, "touch drag scrolls the details")
CrashScreen.pointerReleased(longScreen, "touch")
check(longScreen.drag == nil, "releasing touch ends the drag")
local copiedPath
love.system.setClipboardText = function(value) copiedPath = value end
love.keyboard = { isDown = function() return true end }
love.window.isOpen = function() return true end
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "keypressed", "c"
end
end
end
drawn = {}
local copyLoop = love.errorhandler("src/core/Game2.lua:42: crash")
copyLoop()
eq(copiedPath, "/tmp/pokeport-stub-save/lua-error.log",
"Ctrl+C copies the exact log path")
display = table.concat(drawn, "\n")
check(display:find("copied to clipboard", 1, true) ~= nil,
"screen confirms the copied path")
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "mousepressed", 515, 430, 1
end
end
end
local clickLoop = love.errorhandler("src/core/Game2.lua:42: crash")
eq(clickLoop(), 1, "clicking Close game exits the crash screen")
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "gamepadpressed", {}, "start"
end
end
end
local padLoop = love.errorhandler("src/core/Game2.lua:42: crash")
eq(padLoop(), 1, "pressing Start exits the crash screen")
local originalScroll = CrashScreen.scroll
local scrollCalls = 0
CrashScreen.scroll = function(screen, amount)
scrollCalls = scrollCalls + 1
originalScroll(screen, amount)
end
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "wheelmoved", 0, -1
end
end
end
local wheelLoop = love.errorhandler("src/core/Game2.lua:42: crash")
wheelLoop()
CrashScreen.scroll = originalScroll
eq(scrollCalls, 1, "mouse wheel events reach the details scroller")
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "mousepressed", 515, 430, 1, true
end
end
end
local syntheticMouseLoop = love.errorhandler("src/core/Game2.lua:42: crash")
eq(syntheticMouseLoop(), nil,
"synthetic mouse presses do not duplicate touch input")
love.event.poll = function()
local sent = false
return function()
if not sent then
sent = true
return "touchpressed", "finger", 0.81, 0.75
end
end
end
local touchLoop = love.errorhandler("src/core/Game2.lua:42: crash")
eq(touchLoop(), 1, "touching Close game exits the crash screen")
T.finish()