You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
# Unit tests for RTTTL parser (RTTTLStream)
|
|
import unittest
|
|
import sys
|
|
|
|
|
|
# Mock hardware before importing
|
|
class MockPWM:
|
|
def __init__(self, pin, freq=0, duty=0):
|
|
self.pin = pin
|
|
self.last_freq = freq
|
|
self.last_duty = duty
|
|
self.freq_history = []
|
|
self.duty_history = []
|
|
|
|
def freq(self, value=None):
|
|
if value is not None:
|
|
self.last_freq = value
|
|
self.freq_history.append(value)
|
|
return self.last_freq
|
|
|
|
def duty_u16(self, value=None):
|
|
if value is not None:
|
|
self.last_duty = value
|
|
self.duty_history.append(value)
|
|
return self.last_duty
|
|
|
|
|
|
# Inject mock
|
|
sys.modules['machine'] = type('module', (), {'PWM': MockPWM, 'Pin': lambda x: x})()
|
|
|
|
|
|
# Now import the module to test
|
|
from mpos.audio.stream_rtttl import RTTTLStream # Keep this as-is since it's a specific internal module
|
|
|
|
|
|
class TestRTTTL(unittest.TestCase):
|
|
"""Test cases for RTTTL parser."""
|
|
|
|
def setUp(self):
|
|
"""Create a mock buzzer before each test."""
|
|
self.buzzer = MockPWM(46)
|
|
|
|
def test_parse_simple_rtttl(self):
|
|
"""Test parsing a simple RTTTL string."""
|
|
rtttl = "Nokia:d=4,o=5,b=225:8e6,8d6,8f#,8g#"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
self.assertEqual(stream.name, "Nokia")
|
|
self.assertEqual(stream.default_duration, 4)
|
|
self.assertEqual(stream.default_octave, 5)
|
|
self.assertEqual(stream.bpm, 225)
|
|
|
|
def test_parse_defaults(self):
|
|
"""Test parsing default values."""
|
|
rtttl = "Test:d=8,o=6,b=180:c"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
self.assertEqual(stream.default_duration, 8)
|
|
self.assertEqual(stream.default_octave, 6)
|
|
self.assertEqual(stream.bpm, 180)
|
|
|
|
# Check calculated msec_per_whole_note
|
|
# 240000 / 180 = 1333.33...
|
|
self.assertAlmostEqual(stream.msec_per_whole_note, 1333.33, places=1)
|
|
|
|
def test_invalid_rtttl_format(self):
|
|
"""Test that invalid RTTTL format raises ValueError."""
|
|
# Missing colons
|
|
with self.assertRaises(ValueError):
|
|
RTTTLStream("invalid", 0, 100, self.buzzer, None)
|
|
|
|
# Too many colons
|
|
with self.assertRaises(ValueError):
|
|
RTTTLStream("a:b:c:d", 0, 100, self.buzzer, None)
|
|
|
|
def test_note_parsing(self):
|
|
"""Test parsing individual notes."""
|
|
rtttl = "Test:d=4,o=5,b=120:c,d,e"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
# Generate notes
|
|
notes = list(stream._notes())
|
|
|
|
# Should have 3 notes
|
|
self.assertEqual(len(notes), 3)
|
|
|
|
# Each note should be a tuple of (frequency, duration)
|
|
for freq, duration in notes:
|
|
self.assertTrue(freq > 0, "Frequency should be non-zero")
|
|
self.assertTrue(duration > 0, "Duration should be non-zero")
|
|
|
|
def test_sharp_notes(self):
|
|
"""Test parsing sharp notes."""
|
|
rtttl = "Test:d=4,o=5,b=120:c#,d#,f#"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
notes = list(stream._notes())
|
|
self.assertEqual(len(notes), 3)
|
|
|
|
# Sharp notes should have different frequencies than natural notes
|
|
# (can't test exact values without knowing frequency table)
|
|
|
|
def test_pause_notes(self):
|
|
"""Test parsing pause notes."""
|
|
rtttl = "Test:d=4,o=5,b=120:c,p,e"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
notes = list(stream._notes())
|
|
self.assertEqual(len(notes), 3)
|
|
|
|
# Pause (p) should have frequency 0
|
|
freq, duration = notes[1]
|
|
self.assertEqual(freq, 0.0)
|
|
|
|
def test_duration_modifiers(self):
|
|
"""Test note duration modifiers (dots)."""
|
|
rtttl = "Test:d=4,o=5,b=120:c,c."
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
notes = list(stream._notes())
|
|
self.assertEqual(len(notes), 2)
|
|
|
|
# Dotted note should be 1.5x longer
|
|
normal_duration = notes[0][1]
|
|
dotted_duration = notes[1][1]
|
|
self.assertAlmostEqual(dotted_duration / normal_duration, 1.5, places=1)
|
|
|
|
def test_octave_variations(self):
|
|
"""Test notes with different octaves."""
|
|
rtttl = "Test:d=4,o=5,b=120:c4,c5,c6,c7"
|
|
stream = RTTTLStream(rtttl, 0, 100, self.buzzer, None)
|
|
|
|
notes = list(stream._notes())
|
|
self.assertEqual(len(notes), 4)
|
|
|
|
# Higher octaves should have higher frequencies
|
|
freqs = [freq for freq, dur in notes]
|
|
self.assertTrue(freqs[0] < freqs[1], "c4 should be lower than c5")
|
|
self.assertTrue(freqs[1] < freqs[2], "c5 should be lower than c6")
|
|
self.assertTrue(freqs[2] < freqs[3], "c6 should be lower than c7")
|
|
|
|
def test_volume_scaling(self):
|
|
"""Test volume to duty cycle conversion."""
|
|
# Test various volume levels
|
|
for volume in [0, 25, 50, 75, 100]:
|
|
stream = RTTTLStream("Test:d=4,o=5,b=120:c", 0, volume, self.buzzer, None)
|
|
|
|
# Volume 0 should result in duty 0
|
|
if volume == 0:
|
|
# Note: play() method calculates duty, not __init__
|
|
pass # Can't easily test without calling play()
|
|
else:
|
|
# Volume > 0 should result in duty > 0
|
|
# (duty calculation happens in play() method)
|
|
pass
|
|
|
|
def test_stream_type(self):
|
|
"""Test that stream type is stored correctly."""
|
|
stream = RTTTLStream("Test:d=4,o=5,b=120:c", 2, 100, self.buzzer, None)
|
|
self.assertEqual(stream.stream_type, 2)
|
|
|
|
def test_stop_flag(self):
|
|
"""Test that stop flag can be set."""
|
|
stream = RTTTLStream("Test:d=4,o=5,b=120:c", 0, 100, self.buzzer, None)
|
|
self.assertTrue(stream._keep_running)
|
|
|
|
stream.stop()
|
|
self.assertFalse(stream._keep_running)
|
|
|
|
def test_is_playing_flag(self):
|
|
"""Test playing flag is initially false."""
|
|
stream = RTTTLStream("Test:d=4,o=5,b=120:c", 0, 100, self.buzzer, None)
|
|
self.assertFalse(stream.is_playing())
|