mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
210 lines
7.5 KiB
Lua
210 lines
7.5 KiB
Lua
-- Quitting has to end the process (#339). Both background love.thread
|
|
-- workers idle in loops that only a { cmd = "quit" } command breaks
|
|
-- (src/core/chip_worker.lua, src/update/check_worker.lua) and LOVE joins
|
|
-- every live thread before the process exits, so with no shutdown the window
|
|
-- closed while the process kept spinning: on Android the relaunched task
|
|
-- re-entered an activity whose native main had already returned. Purely a
|
|
-- port lifecycle concern, so no pokered citation.
|
|
-- ROM-free: a ChipAsm blob plus a fake love.thread.
|
|
-- luajit tests/engine/quit_thread_shutdown.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
love = require("tests.love_stub")
|
|
|
|
-- ------- audio + thread stubs
|
|
|
|
local Source = {}
|
|
Source.__index = Source
|
|
function Source:play() self.playing = true end
|
|
function Source:stop() self.playing = false end
|
|
function Source:isPlaying() return self.playing end
|
|
function Source:getFreeBufferCount() return self.free end
|
|
function Source:queue() self.free = math.max(0, self.free - 1) end
|
|
|
|
local ChipSynth = require("src.core.ChipSynth")
|
|
|
|
love.audio = {
|
|
newQueueableSource = function()
|
|
return setmetatable({ free = ChipSynth.MUSIC_BUFFER_COUNT }, Source)
|
|
end,
|
|
}
|
|
|
|
local channels, threads = {}, {}
|
|
|
|
local Channel = {}
|
|
Channel.__index = Channel
|
|
function Channel:push(msg) self.log[#self.log + 1] = msg end
|
|
function Channel:pop() return table.remove(self.log, 1) end
|
|
function Channel:clear() self.log = {} end
|
|
function Channel:getCount() return #self.log end
|
|
|
|
-- the command channels are read back after shutdown, so keep every command
|
|
-- pushed on them rather than letting pop/clear consume the history
|
|
local Recorder = setmetatable({}, { __index = Channel })
|
|
Recorder.__index = Recorder
|
|
function Recorder:pop() return nil end
|
|
function Recorder:clear() end
|
|
|
|
local function channel(name)
|
|
if not channels[name] then
|
|
local mt = name:find("cmd", 1, true) and Recorder or Channel
|
|
channels[name] = setmetatable({ log = {} }, mt)
|
|
end
|
|
return channels[name]
|
|
end
|
|
|
|
local function commands(name)
|
|
local out = {}
|
|
for _, msg in ipairs(channel(name).log) do
|
|
out[#out + 1] = type(msg) == "table" and msg.cmd or tostring(msg)
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function counted(list, want)
|
|
local n = 0
|
|
for _, cmd in ipairs(list) do
|
|
if cmd == want then n = n + 1 end
|
|
end
|
|
return n
|
|
end
|
|
|
|
love.thread = {
|
|
newThread = function(path)
|
|
local th = { path = path, waited = 0, started = false }
|
|
th.start = function() th.started = true end
|
|
th.getError = function() return nil end
|
|
th.wait = function() th.waited = th.waited + 1 end
|
|
threads[#threads + 1] = th
|
|
return th
|
|
end,
|
|
getChannel = channel,
|
|
}
|
|
|
|
local function threadFor(path)
|
|
for _, th in ipairs(threads) do
|
|
if th.path == path then return th end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- ------- chip audio worker
|
|
|
|
local ChipAsm = require("src.audio.ChipAsm")
|
|
|
|
local song = ChipAsm.song{
|
|
channels = { { hw = 1, program = {
|
|
{ notetype = { speed = 12, volume = 12, fade = 0 } },
|
|
{ octave = 4 },
|
|
{ note = "C", len = 8 },
|
|
{ loop = { count = 0, to = 1 } },
|
|
} } },
|
|
}
|
|
local data = { audio = { songs = { Music_PalletTown = song } } }
|
|
|
|
-- absent before the fix; called through this so the rest of the report still
|
|
-- runs instead of erroring out on the first missing entry point
|
|
local function shutdown(mod)
|
|
if type(mod.shutdown) == "function" then mod.shutdown() end
|
|
end
|
|
|
|
local ChipAudio = require("src.core.ChipAudio")
|
|
check(type(ChipAudio.shutdown) == "function", "ChipAudio exposes shutdown")
|
|
|
|
check(ChipAudio.playMusic(data, song, true) ~= nil,
|
|
"threaded playMusic starts the chip worker")
|
|
local chipThread = threadFor("src/core/chip_worker.lua")
|
|
check(chipThread ~= nil and chipThread.started, "the chip worker is running")
|
|
eq(counted(commands("chipaudio_cmd"), "quit"), 0,
|
|
"nothing tells the chip worker to quit during play")
|
|
|
|
shutdown(ChipAudio)
|
|
local chipCmds = commands("chipaudio_cmd")
|
|
eq(chipCmds[#chipCmds], "quit", "shutdown pushes the chip worker's quit command")
|
|
eq(chipThread.waited, 1, "shutdown joins the chip worker instead of leaving it live")
|
|
|
|
-- a second call must not push onto a channel whose worker is already gone,
|
|
-- and neither must the normal playback calls that survive teardown
|
|
shutdown(ChipAudio)
|
|
ChipAudio.stopMusic()
|
|
ChipAudio.invalidate()
|
|
eq(counted(commands("chipaudio_cmd"), "quit"), 1,
|
|
"shutdown is idempotent and post-shutdown calls stay quiet")
|
|
eq(chipThread.waited, 1, "the joined worker is not waited on twice")
|
|
|
|
-- ------- update check worker
|
|
|
|
local Check = require("src.update.Check")
|
|
check(type(Check.shutdown) == "function", "Check exposes shutdown")
|
|
|
|
Check.start()
|
|
local checkThread = threadFor("src/update/check_worker.lua")
|
|
check(checkThread ~= nil and checkThread.started, "the update worker is running")
|
|
eq(commands("update_check_cmd")[1], "check", "start pushes the check command")
|
|
|
|
shutdown(Check)
|
|
local upCmds = commands("update_check_cmd")
|
|
eq(upCmds[#upCmds], "quit", "shutdown pushes the update worker's quit command")
|
|
eq(checkThread.waited, 1, "shutdown joins the update worker")
|
|
|
|
shutdown(Check)
|
|
Check.download()
|
|
eq(counted(commands("update_check_cmd"), "quit"), 1,
|
|
"shutdown is idempotent and post-shutdown calls stay quiet")
|
|
eq(Check.state().status ~= nil, true, "state() still answers after shutdown")
|
|
|
|
-- ------- the worker loops still break on that command
|
|
|
|
local function source(path)
|
|
local f = io.open(path, "rb")
|
|
check(f ~= nil, path .. " is readable")
|
|
local text = f and f:read("*a") or ""
|
|
if f then f:close() end
|
|
return text
|
|
end
|
|
|
|
local chipSrc = source("src/core/chip_worker.lua")
|
|
check(chipSrc:match('cmd%.cmd == "quit"%s*then%s*\n%s*return true') ~= nil,
|
|
"chip_worker leaves its command loop on quit")
|
|
local checkSrc = source("src/update/check_worker.lua")
|
|
check(checkSrc:match('cmd%.cmd == "quit"%s*then%s*\n%s*break') ~= nil,
|
|
"check_worker leaves its demand loop on quit")
|
|
|
|
-- ------- main.lua wiring
|
|
-- love.quit is the established teardown hook (DiscordPresence already rides
|
|
-- it) and reaches the modules through package.loaded, so a session that never
|
|
-- touched audio or the launcher pays nothing.
|
|
|
|
local mainSrc = source("main.lua")
|
|
local quitHook = mainSrc:match("\nfunction love%.quit%(%).-\nend\n")
|
|
check(quitHook ~= nil, "love.quit is still a single top-level function")
|
|
quitHook = quitHook or ""
|
|
check(quitHook:find('package.loaded["src.core.ChipAudio"].shutdown', 1, true) ~= nil,
|
|
"love.quit shuts the chip worker down")
|
|
check(quitHook:find('package.loaded["src.update.Check"].shutdown', 1, true) ~= nil,
|
|
"love.quit shuts the update worker down")
|
|
|
|
-- The Android half: LOVE keeps the JVM process after the native main returns,
|
|
-- so the quit event exits the process outright. It has to sit after the
|
|
-- love.quit() veto test, or the editor's abort-quit path would die on a quit
|
|
-- it refused.
|
|
local quitBranch = mainSrc:match('if name == "quit" then(.-)\n%s*end\n')
|
|
check(quitBranch ~= nil, "love.run still handles the quit event")
|
|
quitBranch = quitBranch or ""
|
|
local vetoAt = quitBranch:find("not love%.quit()")
|
|
local osAt = quitBranch:find("os%.exit")
|
|
local androidAt = quitBranch:find('getOS() == "Android"', 1, true)
|
|
check(vetoAt ~= nil and osAt ~= nil and vetoAt < osAt,
|
|
"the process exit runs only after love.quit declined to veto")
|
|
check(androidAt ~= nil and androidAt < osAt,
|
|
"the process exit is gated on Android")
|
|
local exits = 0
|
|
for _ in mainSrc:gmatch("os%.exit") do exits = exits + 1 end
|
|
eq(exits, 1, "os.exit appears once, at the quit event")
|
|
|
|
T.finish("quit thread shutdown")
|