#
🧪 Offline Testing & Simulation
Sonora v1.2.7 includes comprehensive offline testing capabilities, allowing you to develop and test applications without requiring a live Lavalink server.
#
Protocol Simulator
The ProtocolSimulator provides a complete offline Lavalink protocol implementation for testing.
#
Basic Setup
from sonora import protocol_simulator
# Start the simulator
await protocol_simulator.start()
# Use Sonora normally - all operations work offline
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(guild_id)
await player.play(track)
# Stop simulator
await protocol_simulator.stop()
#
Fault Injection
# Enable fault injection for testing resilience
protocol_simulator.enable_fault_injection({
"packet_loss": 0.05, # 5% packet loss
"latency_spike": 0.1, # 10% latency spikes (5-20x normal)
"connection_drop": 0.02 # 2% connection drops
})
# Run tests with faults
await run_resilience_tests()
# Disable faults
protocol_simulator.disable_fault_injection()
#
Mock Factory
The MockFactory generates realistic test data for comprehensive testing.
#
Mock Tracks
from sonora import mock_factory
# Create individual mock tracks
track1 = mock_factory.create_mock_track(
title="Test Track",
author="Test Artist",
length=180000 # 3 minutes
)
track2 = mock_factory.create_mock_track() # Random data
# Create playlists
playlist = mock_factory.create_mock_playlist(
name="Test Playlist",
track_count=10
)
# Create search results
search_results = mock_factory.create_mock_search_results(
query="test query",
count=20
)
#
Integration Testing
class MusicBotTester:
def __init__(self):
self.simulator = protocol_simulator
self.mock_factory = mock_factory
async def setup_test_environment(self):
"""Set up complete test environment"""
await self.simulator.start()
# Create mock client
self.client = SonoraClient([{
"host": "127.0.0.1",
"port": 2333,
"password": "test"
}])
await self.client.start()
async def test_playback_flow(self):
"""Test complete playback flow"""
# Create player
player = await self.client.get_player(123)
# Test track loading
track = self.mock_factory.create_mock_track()
await player.play(track)
# Verify state
assert player.current_track is not None
assert player.current_track.title == track.title
# Test queue operations
track2 = self.mock_factory.create_mock_track()
await player.queue.add(track2)
assert len(player.queue.upcoming) == 1
# Test skip
await player.skip()
assert player.current_track.title == track2.title
async def test_error_conditions(self):
"""Test error handling"""
player = await self.client.get_player(123)
# Test invalid track
try:
await player.play(None)
assert False, "Should have raised error"
except Exception:
pass # Expected
# Test queue limits
for i in range(150): # Exceed typical limits
track = self.mock_factory.create_mock_track()
await player.queue.add(track)
# Verify queue management
assert len(player.queue.upcoming) <= 1000 # Should be limited
async def test_concurrent_operations(self):
"""Test concurrent operations"""
player = await self.client.get_player(123)
# Create multiple concurrent operations
tasks = []
for i in range(10):
track = self.mock_factory.create_mock_track()
task = player.queue.add(track)
tasks.append(task)
# Execute concurrently
await asyncio.gather(*tasks)
# Verify all tracks added
assert len(player.queue.upcoming) == 10
async def cleanup(self):
"""Clean up test environment"""
await self.client.close()
await self.simulator.stop()
#
CI/CD Integration
#
GitHub Actions Testing
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ ERROR }}
uses: actions/setup-python@v4
with:
python-version: ${{ ERROR }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run offline tests
run: |
python -m pytest tests/ -v --offline-mode
env:
SONORA_OFFLINE_TESTING: true
#
Offline Test Runner
import os
import asyncio
from sonora import protocol_simulator, mock_factory
class OfflineTestRunner:
def __init__(self):
self.simulator = protocol_simulator
self.mock_factory = mock_factory
self.results = []
async def run_all_tests(self):
"""Run complete test suite offline"""
print("๐งช Starting offline test suite...")
# Start simulator
await self.simulator.start()
try:
# Basic functionality tests
await self.test_basic_functionality()
await self.test_queue_operations()
await self.test_filter_system()
await self.test_autoplay_system()
await self.test_error_handling()
await self.test_concurrent_operations()
await self.test_fault_tolerance()
# Performance tests
await self.test_performance_under_load()
await self.test_memory_usage()
await self.test_connection_stability()
finally:
await self.simulator.stop()
# Report results
self.print_results()
async def test_basic_functionality(self):
"""Test basic player functionality"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
track = self.mock_factory.create_mock_track()
await player.play(track)
assert player.current_track is not None
await player.pause()
assert player.paused
await player.resume()
assert not player.paused
await client.close()
self.results.append(("Basic Functionality", True, None))
except Exception as e:
self.results.append(("Basic Functionality", False, str(e)))
async def test_queue_operations(self):
"""Test queue operations"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Add multiple tracks
tracks = [self.mock_factory.create_mock_track() for _ in range(5)]
for track in tracks:
await player.queue.add(track)
assert len(player.queue.upcoming) == 5
# Test shuffle
await player.queue.smart_shuffle()
assert len(player.queue.upcoming) == 5 # Same count, different order
# Test skip through queue
for _ in range(5):
await player.skip()
assert len(player.queue.upcoming) == 0
await client.close()
self.results.append(("Queue Operations", True, None))
except Exception as e:
self.results.append(("Queue Operations", False, str(e)))
async def test_filter_system(self):
"""Test audio filter system"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
track = self.mock_factory.create_mock_track()
await player.play(track)
# Test bass boost
player.filters.bass_boost("high")
await player.set_filters()
# Test nightcore
player.filters.nightcore()
await player.set_filters()
# Test equalizer
from sonora import Equalizer
eq = Equalizer()
eq.set_band(0, 0.5) # Boost bass
player.filters.set_filter(eq)
await client.close()
self.results.append(("Filter System", True, None))
except Exception as e:
self.results.append(("Filter System", False, str(e)))
async def test_autoplay_system(self):
"""Test autoplay functionality"""
try:
from sonora import AutoplayEngine
autoplay = AutoplayEngine(123)
# Configure autoplay
autoplay.configure({
"enabled": True,
"strategy": "similar_artist",
"max_history": 10
})
# Test track recommendation (would need mock context)
context = {
"history": [self.mock_factory.create_mock_track() for _ in range(3)],
"current": self.mock_factory.create_mock_track()
}
# Note: Actual recommendation requires external APIs
# This tests the configuration and setup
assert autoplay.enabled
assert autoplay.strategy == "similar_artist"
self.results.append(("Autoplay System", True, None))
except Exception as e:
self.results.append(("Autoplay System", False, str(e)))
async def test_error_handling(self):
"""Test error handling and recovery"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Test invalid operations
try:
await player.play(None)
assert False, "Should have failed"
except:
pass # Expected
# Test queue edge cases
await player.queue.clear()
assert len(player.queue.upcoming) == 0
await player.skip() # Skip with empty queue
await client.close()
self.results.append(("Error Handling", True, None))
except Exception as e:
self.results.append(("Error Handling", False, str(e)))
async def test_concurrent_operations(self):
"""Test concurrent operations"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Create concurrent operations
async def add_track():
track = self.mock_factory.create_mock_track()
await player.queue.add(track)
async def skip_track():
await asyncio.sleep(0.01) # Small delay
await player.skip()
# Run concurrently
tasks = [add_track() for _ in range(10)] + [skip_track() for _ in range(5)]
await asyncio.gather(*tasks, return_exceptions=True)
await client.close()
self.results.append(("Concurrent Operations", True, None))
except Exception as e:
self.results.append(("Concurrent Operations", False, str(e)))
async def test_fault_tolerance(self):
"""Test fault tolerance with injected faults"""
try:
# Enable faults
self.simulator.enable_fault_injection({
"packet_loss": 0.1,
"latency_spike": 0.2,
"connection_drop": 0.05
})
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Test operations under fault conditions
for i in range(10):
try:
track = self.mock_factory.create_mock_track()
await player.queue.add(track)
await player.skip()
except Exception:
# Some operations may fail due to faults - this is expected
pass
await client.close()
# Disable faults
self.simulator.disable_fault_injection()
self.results.append(("Fault Tolerance", True, None))
except Exception as e:
self.results.append(("Fault Tolerance", False, str(e)))
async def test_performance_under_load(self):
"""Test performance under load"""
try:
import time
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Performance test: add many tracks quickly
start_time = time.time()
tracks = [self.mock_factory.create_mock_track() for _ in range(100)]
tasks = [player.queue.add(track) for track in tracks]
await asyncio.gather(*tasks)
end_time = time.time()
duration = end_time - start_time
# Should complete in reasonable time
assert duration < 5.0 # Less than 5 seconds
assert len(player.queue.upcoming) == 100
await client.close()
self.results.append(("Performance Under Load", True, f"{duration:.2f}s"))
except Exception as e:
self.results.append(("Performance Under Load", False, str(e)))
async def test_memory_usage(self):
"""Test memory usage patterns"""
try:
import psutil
import os
process = psutil.Process(os.getpid())
initial_memory = process.memory_info().rss / 1024 / 1024 # MB
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Add many tracks to test memory usage
tracks = [self.mock_factory.create_mock_track() for _ in range(500)]
tasks = [player.queue.add(track) for track in tracks]
await asyncio.gather(*tasks)
peak_memory = process.memory_info().rss / 1024 / 1024 # MB
memory_increase = peak_memory - initial_memory
# Memory increase should be reasonable (< 50MB for 500 tracks)
assert memory_increase < 50.0
await client.close()
self.results.append(("Memory Usage", True, f"+{memory_increase:.1f}MB"))
except Exception as e:
self.results.append(("Memory Usage", False, str(e)))
async def test_connection_stability(self):
"""Test connection stability under various conditions"""
try:
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Test rapid connect/disconnect cycles
for i in range(5):
# Simulate connection issues
if hasattr(player, '_connected'):
player._connected = False
await asyncio.sleep(0.1)
player._connected = True
# Try operations during connection changes
track = self.mock_factory.create_mock_track()
await player.queue.add(track)
await client.close()
self.results.append(("Connection Stability", True, None))
except Exception as e:
self.results.append(("Connection Stability", False, str(e)))
def print_results(self):
"""Print test results"""
print("\n" + "="*50)
print("๐งช OFFLINE TEST RESULTS")
print("="*50)
passed = 0
failed = 0
for test_name, success, details in self.results:
status = "โ
PASS" if success else "โ FAIL"
print(f"{status} {test_name}")
if details:
print(f" {details}")
print()
if success:
passed += 1
else:
failed += 1
print(f"๐ Summary: {passed} passed, {failed} failed")
print("="*50)
# Run offline tests
if __name__ == "__main__":
runner = OfflineTestRunner()
asyncio.run(runner.run_all_tests())
#
Custom Test Scenarios
#
Scenario-Based Testing
class TestScenarios:
def __init__(self, simulator, mock_factory):
self.simulator = simulator
self.mock_factory = mock_factory
async def scenario_large_guild(self):
"""Test with many users in a guild"""
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
# Simulate 100 concurrent users
players = []
for i in range(100):
player = await client.get_player(i + 1000)
players.append(player)
# All users add tracks simultaneously
tasks = []
for player in players:
track = self.mock_factory.create_mock_track()
task = player.queue.add(track)
tasks.append(task)
await asyncio.gather(*tasks)
# Verify all queues have tracks
for player in players:
assert len(player.queue.upcoming) == 1
await client.close()
async def scenario_high_frequency(self):
"""Test high-frequency operations"""
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Rapid skip operations
track = self.mock_factory.create_mock_track()
await player.play(track)
for i in range(50):
next_track = self.mock_factory.create_mock_track()
await player.queue.add(next_track)
await player.skip()
await client.close()
async def scenario_memory_pressure(self):
"""Test under memory pressure"""
import gc
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
player = await client.get_player(123)
# Add thousands of tracks
tracks = [self.mock_factory.create_mock_track() for _ in range(2000)]
tasks = [player.queue.add(track) for track in tracks]
await asyncio.gather(*tasks)
# Force garbage collection
gc.collect()
# Verify system stability
assert len(player.queue.upcoming) > 0
await client.close()
#
Integration with Testing Frameworks
#
Pytest Integration
# conftest.py
import pytest
import asyncio
from sonora import protocol_simulator, mock_factory
@pytest.fixture(scope="session")
async def simulator():
"""Provide protocol simulator for tests"""
await protocol_simulator.start()
yield protocol_simulator
await protocol_simulator.stop()
@pytest.fixture
def mock_track():
"""Provide mock track for tests"""
return mock_factory.create_mock_track()
@pytest.fixture
async def test_client():
"""Provide test client"""
client = SonoraClient([{"host": "127.0.0.1", "port": 2333, "password": "test"}])
await client.start()
yield client
await client.close()
# test_example.py
@pytest.mark.asyncio
async def test_player_operations(test_client, mock_track):
"""Test player operations"""
player = await test_client.get_player(123)
await player.play(mock_track)
assert player.current_track is not None
await player.pause()
assert player.paused
await player.queue.add(mock_track)
assert len(player.queue.upcoming) == 1
#
CI/CD Best Practices
# Comprehensive CI/CD pipeline
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ ERROR }}
uses: actions/setup-python@v4
with:
python-version: ${{ ERROR }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run offline tests
run: python -m pytest tests/ -v --tb=short
env:
SONORA_OFFLINE_MODE: true
- name: Run performance tests
run: python -m pytest tests/ -k "performance" -v
- name: Run integration tests
run: python -m pytest tests/ -k "integration" -v
- name: Generate test report
run: |
python -m pytest tests/ --cov=sonora --cov-report=html
python -m pytest tests/ --cov=sonora --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Offline testing and simulation in Sonora v1.2.7 enables comprehensive, reliable testing without external dependencies, ensuring your applications work correctly in all scenarios.