All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m35s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
329 lines
10 KiB
Python
329 lines
10 KiB
Python
# SPDX-License-Identifier: MIT
|
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
|
|
|
import app
|
|
import builtins
|
|
import pytest # type: ignore
|
|
import time
|
|
import utils
|
|
|
|
|
|
@pytest.fixture
|
|
def micropythonify():
|
|
def time_ticks_ms():
|
|
return time.time_ns() // 1000000
|
|
time.ticks_ms = time_ticks_ms
|
|
yield
|
|
del time.ticks_ms
|
|
|
|
|
|
class FakeFile:
|
|
def __init__(self, filename, mode):
|
|
self.filename = filename
|
|
self.mode = mode
|
|
self.closed = False
|
|
|
|
def close(self):
|
|
self.closed = True
|
|
|
|
|
|
class FakeMp3Player:
|
|
def __init__(self):
|
|
self.volume: int | None = None
|
|
self.track: FakeFile | None = None
|
|
|
|
def set_volume(self, vol: int):
|
|
self.volume = vol
|
|
|
|
def play(self, track: FakeFile, offset: int):
|
|
self.track = track
|
|
|
|
def stop(self):
|
|
self.track = None
|
|
|
|
|
|
class FakeTimerManager:
|
|
def __init__(self):
|
|
self.queued = []
|
|
|
|
def cancel(self, timer):
|
|
self.queued = [(elem[0], elem[1], True) if elem[1] == timer else elem for elem in self.queued]
|
|
|
|
def schedule(self, when, what):
|
|
self.queued.append((when, what, False))
|
|
|
|
def testing_run_queued(self):
|
|
queued = self.queued
|
|
self.queued = []
|
|
for when, what, canceled in queued:
|
|
if not canceled:
|
|
what()
|
|
|
|
|
|
class FakeNfcReader:
|
|
tag_callback = None
|
|
|
|
def __init__(self, tag_callback=None):
|
|
FakeNfcReader.tag_callback = tag_callback
|
|
|
|
|
|
class FakeButtons:
|
|
def __init__(self): pass
|
|
|
|
|
|
class FakePlaylistDb:
|
|
class FakePlaylist:
|
|
def __init__(self, parent, pos=0):
|
|
self.parent = parent
|
|
self.pos = 0
|
|
|
|
def getCurrentPath(self):
|
|
return self.parent.tracklist[self.pos]
|
|
|
|
def getNextPath(self):
|
|
self.pos += 1
|
|
if self.pos >= len(self.parent.tracklist):
|
|
return None
|
|
return self.parent.tracklist[self.pos]
|
|
|
|
def getPlaybackOffset(self):
|
|
return 0
|
|
|
|
def __init__(self, tracklist=[b'test/path.mp3']):
|
|
self.tracklist = tracklist
|
|
|
|
def getPlaylistForTag(self, tag: bytes):
|
|
return self.FakePlaylist(self)
|
|
|
|
def getSetting(self, key: bytes | str):
|
|
if key == 'tagmode':
|
|
return 'tagremains'
|
|
return None
|
|
|
|
|
|
class FakeHwconfig:
|
|
def __init__(self):
|
|
self.powered = True
|
|
self.on_battery = False
|
|
|
|
def power_off(self):
|
|
self.powered = False
|
|
|
|
def get_on_battery(self):
|
|
return self.on_battery
|
|
|
|
|
|
class FakeLeds:
|
|
IDLE = 0
|
|
PLAYING = 1
|
|
|
|
def __init__(self):
|
|
self.state = None
|
|
|
|
def set_state(self, state):
|
|
self.state = state
|
|
|
|
|
|
class FakeConfig:
|
|
def __init__(self): pass
|
|
|
|
def get_led_count(self):
|
|
return 1
|
|
|
|
def get_idle_timeout(self):
|
|
return 60
|
|
|
|
def get_tag_timeout(self):
|
|
return 5
|
|
|
|
def get_tagmode(self):
|
|
return 'tagremains'
|
|
|
|
|
|
def fake_open(filename, mode):
|
|
return FakeFile(filename, mode)
|
|
|
|
|
|
@pytest.fixture
|
|
def faketimermanager(monkeypatch):
|
|
fake_timer_manager = FakeTimerManager()
|
|
monkeypatch.setattr(utils.timer.TimerManager, '_instance', fake_timer_manager)
|
|
yield fake_timer_manager
|
|
|
|
|
|
def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons,
|
|
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds, config=FakeConfig):
|
|
return app.Dependencies(mp3player=lambda _: mp3player() if callable(mp3player) else mp3player,
|
|
nfcreader=lambda x: nfcreader(x) if callable(nfcreader) else nfcreader,
|
|
buttons=lambda _: buttons() if callable(buttons) else buttons,
|
|
playlistdb=lambda _: playlistdb() if callable(playlistdb) else playlistdb,
|
|
hwconfig=lambda _: hwconfig() if callable(hwconfig) else hwconfig,
|
|
leds=lambda _: leds() if callable(leds) else leds,
|
|
config=lambda _: config() if callable(config) else config)
|
|
|
|
|
|
def test_construct_app(micropythonify, faketimermanager):
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3)
|
|
dut = app.PlayerApp(deps)
|
|
fake_mp3 = dut.player
|
|
assert fake_mp3.volume is not None
|
|
|
|
|
|
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
|
|
fake_db = FakePlaylistDb()
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
|
app.PlayerApp(deps)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
assert "r" in fake_mp3.track.mode
|
|
assert "b" in fake_mp3.track.mode
|
|
|
|
|
|
def test_playlist_seq(micropythonify, faketimermanager, monkeypatch):
|
|
fake_db = FakePlaylistDb([b'track1.mp3', b'track2.mp3', b'track3.mp3'])
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
|
dut = app.PlayerApp(deps)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'track1.mp3'
|
|
|
|
fake_mp3.track = None
|
|
dut.onPlaybackDone()
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'track2.mp3'
|
|
|
|
fake_mp3.track = None
|
|
dut.onPlaybackDone()
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'track3.mp3'
|
|
|
|
fake_mp3.track = None
|
|
dut.onPlaybackDone()
|
|
assert fake_mp3.track is None
|
|
|
|
|
|
def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
|
|
class FakeNoPlaylistDb:
|
|
def getPlaylistForTag(self, tag):
|
|
return None
|
|
|
|
def getSetting(self, key: bytes | str):
|
|
return None
|
|
|
|
fake_db = FakeNoPlaylistDb()
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
|
app.PlayerApp(deps)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is None
|
|
|
|
|
|
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
|
class FakeStartStopConfig(FakeConfig):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def get_tagmode(self):
|
|
return 'tagstartstop'
|
|
|
|
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db, config=FakeStartStopConfig)
|
|
app.PlayerApp(deps)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
# Present tag to start playback
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
# Removing tag should not stop playback
|
|
FakeNfcReader.tag_callback.onTagChange(None)
|
|
faketimermanager.testing_run_queued()
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
# Presenting tag should stop playback
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is None
|
|
# Nothing should change here
|
|
FakeNfcReader.tag_callback.onTagChange(None)
|
|
faketimermanager.testing_run_queued()
|
|
assert fake_mp3.track is None
|
|
# Presenting tag again should start playback again
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
|
|
|
|
def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
|
|
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
|
fake_mp3 = FakeMp3Player()
|
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
|
app.PlayerApp(deps)
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
# Present tag to start playback
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
# Remove tag to stop playback
|
|
FakeNfcReader.tag_callback.onTagChange(None)
|
|
faketimermanager.testing_run_queued()
|
|
assert fake_mp3.track is None
|
|
# Presenting tag again should start playback again
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_mp3.track is not None
|
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
|
|
|
|
|
def test_led_state(micropythonify, faketimermanager, monkeypatch):
|
|
fake_leds = FakeLeds()
|
|
deps = _makedeps(leds=fake_leds)
|
|
app.PlayerApp(deps)
|
|
assert fake_leds.state == FakeLeds.IDLE
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
assert fake_leds.state == FakeLeds.PLAYING
|
|
FakeNfcReader.tag_callback.onTagChange(None)
|
|
faketimermanager.testing_run_queued()
|
|
assert fake_leds.state == FakeLeds.IDLE
|
|
|
|
|
|
def test_idle_shutdown_after_start(micropythonify, faketimermanager, monkeypatch):
|
|
fake_hwconfig = FakeHwconfig()
|
|
fake_hwconfig.on_battery = True
|
|
deps = _makedeps(hwconfig=fake_hwconfig)
|
|
app.PlayerApp(deps)
|
|
assert fake_hwconfig.powered
|
|
faketimermanager.testing_run_queued()
|
|
assert not fake_hwconfig.powered
|
|
|
|
|
|
def test_idle_shutdown_after_playback(micropythonify, faketimermanager, monkeypatch):
|
|
fake_hwconfig = FakeHwconfig()
|
|
fake_hwconfig.on_battery = True
|
|
deps = _makedeps(hwconfig=fake_hwconfig)
|
|
app.PlayerApp(deps)
|
|
assert fake_hwconfig.powered
|
|
with monkeypatch.context() as m:
|
|
m.setattr(builtins, 'open', fake_open)
|
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
|
faketimermanager.testing_run_queued()
|
|
assert fake_hwconfig.powered
|
|
# Stop playback
|
|
FakeNfcReader.tag_callback.onTagChange(None)
|
|
faketimermanager.testing_run_queued()
|
|
# Elapse idle timer
|
|
faketimermanager.testing_run_queued()
|
|
assert not fake_hwconfig.powered
|