local createLibTest = require("/apis/libtest") local createMusic = require("/apis/libmusic") local testlib = createLibTest({ ... }) local function fakeFs(dir, entries) return { isDir = function(path) return path == dir end, list = function(path) if path ~= dir then error("unexpected list: " .. tostring(path)) end return entries end, } end testlib.test("listTracks filters dfpwm, strips extension and sorts", function() local music = createMusic({ dir = "/data/musics", fs = fakeFs("/data/musics", { "Zebra.dfpwm", "notes.txt", "Apple.dfpwm" }), }) local tracks = music.listTracks() testlib.assertEquals(#tracks, 2) testlib.assertEquals(tracks[1].name, "Apple") testlib.assertEquals(tracks[1].path, "/data/musics/Apple.dfpwm") testlib.assertEquals(tracks[2].name, "Zebra") testlib.assertEquals(tracks[2].path, "/data/musics/Zebra.dfpwm") end) testlib.test("listTracks returns empty when directory is missing", function() local music = createMusic({ dir = "/data/musics", fs = { isDir = function() return false end, }, }) testlib.assertEquals(#music.listTracks(), 0) end) -- A speaker whose queue accepts `capacity` buffers before refusing. Tests -- simulate a drained queue by resetting `queued` before firing the captured -- `speaker_audio_empty` handler. local function fakeSpeaker(capacity) local s = { played = {}, stops = 0, queued = 0, capacity = capacity or 1 } s.playAudio = function(buffer) if s.queued >= s.capacity then return false end s.queued = s.queued + 1 s.played[#s.played + 1] = buffer return true end s.stop = function() s.stops = s.stops + 1 end return s end local function fakeHandle(chunks) local index = 0 return { closed = false, read = function(_, _) index = index + 1 return chunks[index] end, close = function(self) self.closed = true end, } end -- identity decoder: the played buffers equal the raw chunks for easy asserts local fakeDfpwm = { make_decoder = function() return function(chunk) return chunk end end, } local function makePlayer(opts) local captured = {} local eventloop = { register = function(event, handler) captured[event] = handler return function() end end, } local music = createMusic({ peripheral = { find = function(name) return name == "speaker" and opts.speaker or nil end, }, io = { open = function(path) return opts.handles[path] end, }, dfpwm = fakeDfpwm, }) local player = music.createPlayer(eventloop) return player, captured, eventloop end testlib.test("play opens the file, primes the speaker and reports playing", function() local speaker = fakeSpeaker(1) local handle = fakeHandle({ "a", "b" }) local player = makePlayer({ speaker = speaker, handles = { ["/song.dfpwm"] = handle } }) player.play("/song.dfpwm") testlib.assertEquals(player.getStatus(), "playing") testlib.assertEquals(player.getCurrentPath(), "/song.dfpwm") testlib.assertEquals(#speaker.played, 1) testlib.assertEquals(speaker.played[1], "a") end) testlib.test("one chunk fed per speaker_audio_empty until EOF", function() local speaker = fakeSpeaker(1) local handle = fakeHandle({ "a", "b" }) local player, captured = makePlayer({ speaker = speaker, handles = { ["/song.dfpwm"] = handle } }) player.play("/song.dfpwm") -- 'a' queued; nothing read ahead testlib.assertEquals(#speaker.played, 1) speaker.queued = 0 -- simulate the queue draining captured.speaker_audio_empty() testlib.assertEquals(speaker.played[2], "b") -- next chunk fed, one per event testlib.assertEquals(player.getStatus(), "playing") -- not yet at EOF speaker.queued = 0 captured.speaker_audio_empty() -- reads nil, hits EOF testlib.assertEquals(#speaker.played, 2) -- nothing extra queued testlib.assertEquals(player.getStatus(), "stopped") end) testlib.test("pause stops feeding and resume loses no buffered chunk", function() local speaker = fakeSpeaker(1) local handle = fakeHandle({ "a", "b", "c" }) local player, captured = makePlayer({ speaker = speaker, handles = { ["/song.dfpwm"] = handle } }) player.play("/song.dfpwm") -- 'a' queued, nothing read ahead player.pause() testlib.assertEquals(player.getStatus(), "paused") speaker.queued = 0 captured.speaker_audio_empty() -- no-op while paused testlib.assertEquals(#speaker.played, 1) player.resume() testlib.assertEquals(player.getStatus(), "playing") testlib.assertEquals(speaker.played[2], "b") -- resume feeds next chunk, nothing dropped end) testlib.test("stop clears the speaker, closes the handle and resets state", function() local speaker = fakeSpeaker(1) local handle = fakeHandle({ "a", "b" }) local player = makePlayer({ speaker = speaker, handles = { ["/song.dfpwm"] = handle } }) player.play("/song.dfpwm") player.stop() testlib.assertEquals(speaker.stops, 1) testlib.assertTrue(handle.closed, "handle should be closed") testlib.assertEquals(player.getStatus(), "stopped") testlib.assertEquals(player.getCurrentPath(), nil) end) testlib.test("reaching the end fires onFinish once and stays stopped", function() local speaker = fakeSpeaker(1) local handle = fakeHandle({ "a", "b" }) local player, captured = makePlayer({ speaker = speaker, handles = { ["/song.dfpwm"] = handle } }) local finishes = 0 player.onFinish(function() finishes = finishes + 1 end) player.play("/song.dfpwm") -- 'a' queued -- One empty event per remaining chunk, then one more that reads nil (EOF). for _ = 1, 2 do speaker.queued = 0 captured.speaker_audio_empty() end testlib.assertEquals(finishes, 1) testlib.assertEquals(player.getStatus(), "stopped") testlib.assertEquals(#speaker.played, 2) end) testlib.run()