You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
API: add AudioFlinger for audio playback (i2s DAC and buzzer)
API: add LightsManager for multicolor LEDs
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# Hardware Mocks for Testing AudioFlinger and LightsManager
|
||||
# Provides mock implementations of PWM, I2S, NeoPixel, and Pin classes
|
||||
|
||||
|
||||
class MockPin:
|
||||
"""Mock machine.Pin for testing."""
|
||||
|
||||
IN = 0
|
||||
OUT = 1
|
||||
PULL_UP = 2
|
||||
|
||||
def __init__(self, pin_number, mode=None, pull=None):
|
||||
self.pin_number = pin_number
|
||||
self.mode = mode
|
||||
self.pull = pull
|
||||
self._value = 0
|
||||
|
||||
def value(self, val=None):
|
||||
if val is not None:
|
||||
self._value = val
|
||||
return self._value
|
||||
|
||||
|
||||
class MockPWM:
|
||||
"""Mock machine.PWM for testing buzzer."""
|
||||
|
||||
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):
|
||||
"""Set or get frequency."""
|
||||
if value is not None:
|
||||
self.last_freq = value
|
||||
self.freq_history.append(value)
|
||||
return self.last_freq
|
||||
|
||||
def duty_u16(self, value=None):
|
||||
"""Set or get duty cycle (0-65535)."""
|
||||
if value is not None:
|
||||
self.last_duty = value
|
||||
self.duty_history.append(value)
|
||||
return self.last_duty
|
||||
|
||||
|
||||
class MockI2S:
|
||||
"""Mock machine.I2S for testing audio playback."""
|
||||
|
||||
TX = 0
|
||||
MONO = 1
|
||||
STEREO = 2
|
||||
|
||||
def __init__(self, id, sck, ws, sd, mode, bits, format, rate, ibuf):
|
||||
self.id = id
|
||||
self.sck = sck
|
||||
self.ws = ws
|
||||
self.sd = sd
|
||||
self.mode = mode
|
||||
self.bits = bits
|
||||
self.format = format
|
||||
self.rate = rate
|
||||
self.ibuf = ibuf
|
||||
self.written_bytes = []
|
||||
self.total_bytes_written = 0
|
||||
|
||||
def write(self, buf):
|
||||
"""Simulate writing to I2S hardware."""
|
||||
self.written_bytes.append(bytes(buf))
|
||||
self.total_bytes_written += len(buf)
|
||||
return len(buf)
|
||||
|
||||
def deinit(self):
|
||||
"""Deinitialize I2S."""
|
||||
pass
|
||||
|
||||
|
||||
class MockNeoPixel:
|
||||
"""Mock neopixel.NeoPixel for testing LEDs."""
|
||||
|
||||
def __init__(self, pin, num_leds):
|
||||
self.pin = pin
|
||||
self.num_leds = num_leds
|
||||
self.pixels = [(0, 0, 0)] * num_leds
|
||||
self.write_count = 0
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
"""Set LED color (R, G, B) tuple."""
|
||||
if 0 <= index < self.num_leds:
|
||||
self.pixels[index] = value
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Get LED color."""
|
||||
if 0 <= index < self.num_leds:
|
||||
return self.pixels[index]
|
||||
return (0, 0, 0)
|
||||
|
||||
def write(self):
|
||||
"""Update hardware (mock - just increment counter)."""
|
||||
self.write_count += 1
|
||||
@@ -0,0 +1,243 @@
|
||||
# Unit tests for AudioFlinger service
|
||||
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
|
||||
|
||||
def freq(self, value=None):
|
||||
if value is not None:
|
||||
self.last_freq = value
|
||||
return self.last_freq
|
||||
|
||||
def duty_u16(self, value=None):
|
||||
if value is not None:
|
||||
self.last_duty = value
|
||||
return self.last_duty
|
||||
|
||||
|
||||
class MockPin:
|
||||
IN = 0
|
||||
OUT = 1
|
||||
|
||||
def __init__(self, pin_number, mode=None):
|
||||
self.pin_number = pin_number
|
||||
self.mode = mode
|
||||
|
||||
|
||||
# Inject mocks
|
||||
class MockMachine:
|
||||
PWM = MockPWM
|
||||
Pin = MockPin
|
||||
sys.modules['machine'] = MockMachine()
|
||||
|
||||
class MockLock:
|
||||
def acquire(self):
|
||||
pass
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
class MockThread:
|
||||
@staticmethod
|
||||
def allocate_lock():
|
||||
return MockLock()
|
||||
@staticmethod
|
||||
def start_new_thread(func, args, **kwargs):
|
||||
pass # No-op for testing
|
||||
@staticmethod
|
||||
def stack_size(size=None):
|
||||
return 16384 if size is None else None
|
||||
|
||||
sys.modules['_thread'] = MockThread()
|
||||
|
||||
class MockMposApps:
|
||||
@staticmethod
|
||||
def good_stack_size():
|
||||
return 16384
|
||||
|
||||
sys.modules['mpos.apps'] = MockMposApps()
|
||||
|
||||
|
||||
# Now import the module to test
|
||||
import mpos.audio.audioflinger as AudioFlinger
|
||||
|
||||
|
||||
class TestAudioFlinger(unittest.TestCase):
|
||||
"""Test cases for AudioFlinger service."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize AudioFlinger before each test."""
|
||||
self.buzzer = MockPWM(MockPin(46))
|
||||
self.i2s_pins = {'sck': 2, 'ws': 47, 'sd': 16}
|
||||
|
||||
# Reset volume to default before each test
|
||||
AudioFlinger.set_volume(70)
|
||||
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_BOTH,
|
||||
i2s_pins=self.i2s_pins,
|
||||
buzzer_instance=self.buzzer
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test."""
|
||||
AudioFlinger.stop()
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that AudioFlinger initializes correctly."""
|
||||
self.assertEqual(AudioFlinger.get_device_type(), AudioFlinger.DEVICE_BOTH)
|
||||
self.assertEqual(AudioFlinger._i2s_pins, self.i2s_pins)
|
||||
self.assertEqual(AudioFlinger._buzzer_instance, self.buzzer)
|
||||
|
||||
def test_device_types(self):
|
||||
"""Test device type constants."""
|
||||
self.assertEqual(AudioFlinger.DEVICE_NULL, 0)
|
||||
self.assertEqual(AudioFlinger.DEVICE_I2S, 1)
|
||||
self.assertEqual(AudioFlinger.DEVICE_BUZZER, 2)
|
||||
self.assertEqual(AudioFlinger.DEVICE_BOTH, 3)
|
||||
|
||||
def test_stream_types(self):
|
||||
"""Test stream type constants and priority order."""
|
||||
self.assertEqual(AudioFlinger.STREAM_MUSIC, 0)
|
||||
self.assertEqual(AudioFlinger.STREAM_NOTIFICATION, 1)
|
||||
self.assertEqual(AudioFlinger.STREAM_ALARM, 2)
|
||||
|
||||
# Higher number = higher priority
|
||||
self.assertTrue(AudioFlinger.STREAM_MUSIC < AudioFlinger.STREAM_NOTIFICATION)
|
||||
self.assertTrue(AudioFlinger.STREAM_NOTIFICATION < AudioFlinger.STREAM_ALARM)
|
||||
|
||||
def test_volume_control(self):
|
||||
"""Test volume get/set operations."""
|
||||
# Set volume
|
||||
AudioFlinger.set_volume(50)
|
||||
self.assertEqual(AudioFlinger.get_volume(), 50)
|
||||
|
||||
# Test clamping to 0-100 range
|
||||
AudioFlinger.set_volume(150)
|
||||
self.assertEqual(AudioFlinger.get_volume(), 100)
|
||||
|
||||
AudioFlinger.set_volume(-10)
|
||||
self.assertEqual(AudioFlinger.get_volume(), 0)
|
||||
|
||||
def test_device_null_rejects_playback(self):
|
||||
"""Test that DEVICE_NULL rejects all playback requests."""
|
||||
# Re-initialize with no device
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_NULL,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=None
|
||||
)
|
||||
|
||||
# WAV should be rejected
|
||||
result = AudioFlinger.play_wav("test.wav")
|
||||
self.assertFalse(result)
|
||||
|
||||
# RTTTL should be rejected
|
||||
result = AudioFlinger.play_rtttl("Test:d=4,o=5,b=120:c")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_device_i2s_only_rejects_rtttl(self):
|
||||
"""Test that DEVICE_I2S rejects buzzer playback."""
|
||||
# Re-initialize with I2S only
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_I2S,
|
||||
i2s_pins=self.i2s_pins,
|
||||
buzzer_instance=None
|
||||
)
|
||||
|
||||
# RTTTL should be rejected (no buzzer)
|
||||
result = AudioFlinger.play_rtttl("Test:d=4,o=5,b=120:c")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_device_buzzer_only_rejects_wav(self):
|
||||
"""Test that DEVICE_BUZZER rejects I2S playback."""
|
||||
# Re-initialize with buzzer only
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_BUZZER,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=self.buzzer
|
||||
)
|
||||
|
||||
# WAV should be rejected (no I2S)
|
||||
result = AudioFlinger.play_wav("test.wav")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_missing_i2s_pins_rejects_wav(self):
|
||||
"""Test that missing I2S pins rejects WAV playback."""
|
||||
# Re-initialize with I2S device but no pins
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_I2S,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=None
|
||||
)
|
||||
|
||||
result = AudioFlinger.play_wav("test.wav")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_is_playing_initially_false(self):
|
||||
"""Test that is_playing() returns False initially."""
|
||||
self.assertFalse(AudioFlinger.is_playing())
|
||||
|
||||
def test_stop_with_no_playback(self):
|
||||
"""Test that stop() can be called when nothing is playing."""
|
||||
# Should not raise exception
|
||||
AudioFlinger.stop()
|
||||
self.assertFalse(AudioFlinger.is_playing())
|
||||
|
||||
def test_get_device_type(self):
|
||||
"""Test that get_device_type() returns correct value."""
|
||||
# Test DEVICE_BOTH
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_BOTH,
|
||||
i2s_pins=self.i2s_pins,
|
||||
buzzer_instance=self.buzzer
|
||||
)
|
||||
self.assertEqual(AudioFlinger.get_device_type(), AudioFlinger.DEVICE_BOTH)
|
||||
|
||||
# Test DEVICE_I2S
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_I2S,
|
||||
i2s_pins=self.i2s_pins,
|
||||
buzzer_instance=None
|
||||
)
|
||||
self.assertEqual(AudioFlinger.get_device_type(), AudioFlinger.DEVICE_I2S)
|
||||
|
||||
# Test DEVICE_BUZZER
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_BUZZER,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=self.buzzer
|
||||
)
|
||||
self.assertEqual(AudioFlinger.get_device_type(), AudioFlinger.DEVICE_BUZZER)
|
||||
|
||||
# Test DEVICE_NULL
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_NULL,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=None
|
||||
)
|
||||
self.assertEqual(AudioFlinger.get_device_type(), AudioFlinger.DEVICE_NULL)
|
||||
|
||||
def test_audio_focus_check_no_current_stream(self):
|
||||
"""Test audio focus allows playback when no stream is active."""
|
||||
result = AudioFlinger._check_audio_focus(AudioFlinger.STREAM_MUSIC)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_init_creates_lock(self):
|
||||
"""Test that initialization creates a stream lock."""
|
||||
self.assertIsNotNone(AudioFlinger._stream_lock)
|
||||
|
||||
def test_volume_default_value(self):
|
||||
"""Test that default volume is reasonable."""
|
||||
# After init, volume should be at default (70)
|
||||
AudioFlinger.init(
|
||||
device_type=AudioFlinger.DEVICE_NULL,
|
||||
i2s_pins=None,
|
||||
buzzer_instance=None
|
||||
)
|
||||
self.assertEqual(AudioFlinger.get_volume(), 70)
|
||||
@@ -0,0 +1,126 @@
|
||||
# Unit tests for LightsManager service
|
||||
import unittest
|
||||
import sys
|
||||
|
||||
|
||||
# Mock hardware before importing LightsManager
|
||||
class MockPin:
|
||||
IN = 0
|
||||
OUT = 1
|
||||
|
||||
def __init__(self, pin_number, mode=None):
|
||||
self.pin_number = pin_number
|
||||
self.mode = mode
|
||||
|
||||
|
||||
class MockNeoPixel:
|
||||
def __init__(self, pin, num_leds):
|
||||
self.pin = pin
|
||||
self.num_leds = num_leds
|
||||
self.pixels = [(0, 0, 0)] * num_leds
|
||||
self.write_count = 0
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if 0 <= index < self.num_leds:
|
||||
self.pixels[index] = value
|
||||
|
||||
def __getitem__(self, index):
|
||||
if 0 <= index < self.num_leds:
|
||||
return self.pixels[index]
|
||||
return (0, 0, 0)
|
||||
|
||||
def write(self):
|
||||
self.write_count += 1
|
||||
|
||||
|
||||
# Inject mocks
|
||||
sys.modules['machine'] = type('module', (), {'Pin': MockPin})()
|
||||
sys.modules['neopixel'] = type('module', (), {'NeoPixel': MockNeoPixel})()
|
||||
|
||||
|
||||
# Now import the module to test
|
||||
import mpos.lights as LightsManager
|
||||
|
||||
|
||||
class TestLightsManager(unittest.TestCase):
|
||||
"""Test cases for LightsManager service."""
|
||||
|
||||
def setUp(self):
|
||||
"""Initialize LightsManager before each test."""
|
||||
LightsManager.init(neopixel_pin=12, num_leds=5)
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test that LightsManager initializes correctly."""
|
||||
self.assertTrue(LightsManager.is_available())
|
||||
self.assertEqual(LightsManager.get_led_count(), 5)
|
||||
|
||||
def test_set_single_led(self):
|
||||
"""Test setting a single LED color."""
|
||||
result = LightsManager.set_led(0, 255, 0, 0)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify color was set (via internal _neopixel mock)
|
||||
neopixel = LightsManager._neopixel
|
||||
self.assertEqual(neopixel[0], (255, 0, 0))
|
||||
|
||||
def test_set_led_invalid_index(self):
|
||||
"""Test that invalid LED indices are rejected."""
|
||||
# Negative index
|
||||
result = LightsManager.set_led(-1, 255, 0, 0)
|
||||
self.assertFalse(result)
|
||||
|
||||
# Index too large
|
||||
result = LightsManager.set_led(10, 255, 0, 0)
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_set_all_leds(self):
|
||||
"""Test setting all LEDs to same color."""
|
||||
result = LightsManager.set_all(0, 255, 0)
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify all LEDs were set
|
||||
neopixel = LightsManager._neopixel
|
||||
for i in range(5):
|
||||
self.assertEqual(neopixel[i], (0, 255, 0))
|
||||
|
||||
def test_clear(self):
|
||||
"""Test clearing all LEDs."""
|
||||
# First set some colors
|
||||
LightsManager.set_all(255, 255, 255)
|
||||
|
||||
# Then clear
|
||||
result = LightsManager.clear()
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify all LEDs are black
|
||||
neopixel = LightsManager._neopixel
|
||||
for i in range(5):
|
||||
self.assertEqual(neopixel[i], (0, 0, 0))
|
||||
|
||||
def test_write(self):
|
||||
"""Test that write() updates hardware."""
|
||||
neopixel = LightsManager._neopixel
|
||||
initial_count = neopixel.write_count
|
||||
|
||||
result = LightsManager.write()
|
||||
self.assertTrue(result)
|
||||
|
||||
# Verify write was called
|
||||
self.assertEqual(neopixel.write_count, initial_count + 1)
|
||||
|
||||
def test_notification_colors(self):
|
||||
"""Test convenience notification color method."""
|
||||
# Valid colors
|
||||
self.assertTrue(LightsManager.set_notification_color("red"))
|
||||
self.assertTrue(LightsManager.set_notification_color("green"))
|
||||
self.assertTrue(LightsManager.set_notification_color("blue"))
|
||||
|
||||
# Invalid color
|
||||
result = LightsManager.set_notification_color("invalid_color")
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_case_insensitive_colors(self):
|
||||
"""Test that color names are case-insensitive."""
|
||||
self.assertTrue(LightsManager.set_notification_color("RED"))
|
||||
self.assertTrue(LightsManager.set_notification_color("Green"))
|
||||
self.assertTrue(LightsManager.set_notification_color("BLUE"))
|
||||
@@ -0,0 +1,173 @@
|
||||
# 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
|
||||
|
||||
|
||||
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())
|
||||
@@ -0,0 +1,78 @@
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
class TestSysPathRestore(unittest.TestCase):
|
||||
"""Test that sys.path is properly restored after execute_script"""
|
||||
|
||||
def test_syspath_restored_after_execute_script(self):
|
||||
"""Test that sys.path is restored to original state after script execution"""
|
||||
# Import here to ensure we're in the right context
|
||||
import mpos.apps
|
||||
|
||||
# Capture original sys.path
|
||||
original_path = sys.path[:]
|
||||
original_length = len(sys.path)
|
||||
|
||||
# Create a test directory path that would be added
|
||||
test_cwd = "apps/com.test.app/assets/"
|
||||
|
||||
# Verify the test path is not already in sys.path
|
||||
self.assertFalse(test_cwd in original_path,
|
||||
f"Test path {test_cwd} should not be in sys.path initially")
|
||||
|
||||
# Create a simple test script
|
||||
test_script = '''
|
||||
import sys
|
||||
# Just a simple script that does nothing
|
||||
x = 42
|
||||
'''
|
||||
|
||||
# Call execute_script with cwd parameter
|
||||
# Note: This will fail because there's no Activity to start,
|
||||
# but that's fine - we're testing the sys.path restoration
|
||||
result = mpos.apps.execute_script(
|
||||
test_script,
|
||||
is_file=False,
|
||||
cwd=test_cwd,
|
||||
classname="NonExistentClass"
|
||||
)
|
||||
|
||||
# After execution, sys.path should be restored
|
||||
current_path = sys.path
|
||||
current_length = len(sys.path)
|
||||
|
||||
# Verify sys.path has been restored to original
|
||||
self.assertEqual(current_length, original_length,
|
||||
f"sys.path length should be restored. Original: {original_length}, Current: {current_length}")
|
||||
|
||||
# Verify the test directory is not in sys.path anymore
|
||||
self.assertFalse(test_cwd in current_path,
|
||||
f"Test path {test_cwd} should not be in sys.path after execution. sys.path={current_path}")
|
||||
|
||||
# Verify sys.path matches original
|
||||
self.assertEqual(current_path, original_path,
|
||||
f"sys.path should match original.\nOriginal: {original_path}\nCurrent: {current_path}")
|
||||
|
||||
def test_syspath_not_affected_when_no_cwd(self):
|
||||
"""Test that sys.path is unchanged when cwd is None"""
|
||||
import mpos.apps
|
||||
|
||||
# Capture original sys.path
|
||||
original_path = sys.path[:]
|
||||
|
||||
test_script = '''
|
||||
x = 42
|
||||
'''
|
||||
|
||||
# Call without cwd parameter
|
||||
result = mpos.apps.execute_script(
|
||||
test_script,
|
||||
is_file=False,
|
||||
cwd=None,
|
||||
classname="NonExistentClass"
|
||||
)
|
||||
|
||||
# sys.path should be unchanged
|
||||
self.assertEqual(sys.path, original_path,
|
||||
"sys.path should be unchanged when cwd is None")
|
||||
Reference in New Issue
Block a user