test_music.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für das Musik-System.
  4. Testet:
  5. - Track und TrackMetadata Datenklassen
  6. - LocalFileSource (Dateien scannen)
  7. - AudioDecoder (MP3, Format-Erkennung)
  8. - AudioStreamer und StreamSession
  9. - PlayQueue und Playlist
  10. """
  11. import asyncio
  12. import pytest
  13. import shutil
  14. from pathlib import Path
  15. from datetime import datetime, timedelta
  16. from unittest.mock import AsyncMock, MagicMock
  17. from trixy_core.music.track import Track, TrackMetadata, TrackState
  18. from trixy_core.music.sources.local import LocalFileSource, SUPPORTED_EXTENSIONS
  19. from trixy_core.music.sources.base import SourceState, SourceType
  20. from trixy_core.music.formats.base import AudioFormat, AudioInfo, DecodedAudio, DecoderError
  21. from trixy_core.music.formats.mp3 import MP3Decoder
  22. from trixy_core.music.formats.registry import DecoderRegistry
  23. from trixy_core.music.stream.config import StreamConfig
  24. from trixy_core.music.stream.session import StreamSession, StreamState, StreamStats
  25. from trixy_core.music.stream.streamer import AudioStreamer
  26. from trixy_core.music.queue import PlayQueue, QueueItem, QueueState
  27. from trixy_core.music.playlist.playlist import Playlist, RepeatMode, ShuffleMode
  28. # =============================================================================
  29. # Test Data
  30. # =============================================================================
  31. # Pfad zu echten Musikdateien
  32. MUSIC_DIR = Path(__file__).parent.parent / "assets" / "default" / "music"
  33. def has_music_files() -> bool:
  34. """Prüft ob Musikdateien vorhanden sind."""
  35. return MUSIC_DIR.exists() and any(MUSIC_DIR.glob("*.mp3"))
  36. def get_first_mp3() -> Path | None:
  37. """Holt die erste MP3-Datei."""
  38. if MUSIC_DIR.exists():
  39. mp3_files = list(MUSIC_DIR.glob("*.mp3"))
  40. if mp3_files:
  41. return mp3_files[0]
  42. return None
  43. def has_pydub() -> bool:
  44. """Prüft ob pydub und ffmpeg/ffprobe verfügbar sind."""
  45. try:
  46. from pydub import AudioSegment
  47. import subprocess
  48. # Prüfe auch ob ffprobe verfügbar ist (benötigt für MP3-Dekodierung)
  49. result = subprocess.run(
  50. ["ffprobe", "-version"],
  51. capture_output=True,
  52. timeout=5,
  53. )
  54. return result.returncode == 0
  55. except (ImportError, FileNotFoundError, subprocess.TimeoutExpired):
  56. return False
  57. def has_mutagen() -> bool:
  58. """Prüft ob mutagen verfügbar ist."""
  59. try:
  60. import mutagen
  61. return True
  62. except ImportError:
  63. return False
  64. def has_ffmpeg() -> bool:
  65. """Prüft ob ffmpeg verfügbar ist."""
  66. return shutil.which("ffmpeg") is not None
  67. # =============================================================================
  68. # Tests für TrackMetadata
  69. # =============================================================================
  70. class TestTrackMetadata:
  71. """Tests für TrackMetadata Datenklasse."""
  72. def test_default_values(self):
  73. """Testet Standard-Werte."""
  74. metadata = TrackMetadata()
  75. assert metadata.title == ""
  76. assert metadata.artist == ""
  77. assert metadata.album == ""
  78. assert metadata.sample_rate == 44100
  79. assert metadata.channels == 2
  80. assert metadata.duration_ms == 0
  81. def test_duration_property(self):
  82. """Testet duration Property."""
  83. metadata = TrackMetadata(duration_ms=180000) # 3 Minuten
  84. assert metadata.duration == timedelta(milliseconds=180000)
  85. assert metadata.duration.total_seconds() == 180.0
  86. def test_duration_formatted(self):
  87. """Testet formatierte Dauer."""
  88. metadata = TrackMetadata(duration_ms=185000) # 3:05
  89. assert metadata.duration_formatted == "3:05"
  90. def test_duration_formatted_short(self):
  91. """Testet formatierte Dauer unter einer Minute."""
  92. metadata = TrackMetadata(duration_ms=45000) # 45 Sekunden
  93. assert metadata.duration_formatted == "0:45"
  94. def test_display_artist(self):
  95. """Testet display_artist."""
  96. # Ohne Artist
  97. metadata = TrackMetadata()
  98. assert metadata.display_artist == "Unbekannter Künstler"
  99. # Mit Artist
  100. metadata = TrackMetadata(artist="Test Artist")
  101. assert metadata.display_artist == "Test Artist"
  102. # Album-Artist überschreibt
  103. metadata = TrackMetadata(artist="Track Artist", album_artist="Album Artist")
  104. assert metadata.display_artist == "Album Artist"
  105. def test_display_title(self):
  106. """Testet display_title."""
  107. metadata = TrackMetadata()
  108. assert metadata.display_title == "Unbekannter Titel"
  109. metadata = TrackMetadata(title="Test Song")
  110. assert metadata.display_title == "Test Song"
  111. def test_to_dict(self):
  112. """Testet to_dict Konvertierung."""
  113. metadata = TrackMetadata(
  114. title="Test",
  115. artist="Artist",
  116. duration_ms=120000,
  117. )
  118. result = metadata.to_dict()
  119. assert result["title"] == "Test"
  120. assert result["artist"] == "Artist"
  121. assert result["duration_ms"] == 120000
  122. assert result["duration_formatted"] == "2:00"
  123. assert "artwork_data" not in result # Binärdaten ausgeschlossen
  124. # =============================================================================
  125. # Tests für Track
  126. # =============================================================================
  127. class TestTrack:
  128. """Tests für Track Datenklasse."""
  129. def test_auto_id(self):
  130. """Testet automatische ID-Generierung."""
  131. track = Track()
  132. assert track.id.startswith("track_")
  133. assert len(track.id) == 18 # "track_" + 12 hex chars
  134. def test_uri_from_path(self):
  135. """Testet URI-Generierung aus Pfad."""
  136. track = Track(path=Path("/music/test.mp3"))
  137. assert track.uri.startswith("file://")
  138. assert "test.mp3" in track.uri
  139. def test_uri_from_source(self):
  140. """Testet URI-Generierung aus Source."""
  141. track = Track(source_type="spotify", source_id="abc123")
  142. assert track.uri == "spotify:abc123"
  143. def test_state_transitions(self):
  144. """Testet Zustandsübergänge."""
  145. track = Track()
  146. assert track.state == TrackState.UNKNOWN
  147. track.state = TrackState.AVAILABLE
  148. assert track.is_available is True
  149. assert track.is_playing is False
  150. track.state = TrackState.PLAYING
  151. assert track.is_playing is True
  152. def test_is_local(self):
  153. """Testet is_local Property."""
  154. # Kein lokaler Track
  155. track = Track(source_type="spotify")
  156. assert track.is_local is False
  157. # Lokaler Track ohne Pfad
  158. track = Track(source_type="local")
  159. assert track.is_local is False
  160. # Lokaler Track mit Pfad
  161. track = Track(source_type="local", path=Path("/music/test.mp3"))
  162. assert track.is_local is True
  163. def test_mark_played(self):
  164. """Testet mark_played."""
  165. track = Track()
  166. assert track.play_count == 0
  167. assert track.last_played is None
  168. track.mark_played()
  169. assert track.play_count == 1
  170. assert track.last_played is not None
  171. assert isinstance(track.last_played, datetime)
  172. def test_set_error(self):
  173. """Testet Fehler-Zustand."""
  174. track = Track()
  175. track.set_error("Datei nicht gefunden")
  176. assert track.state == TrackState.ERROR
  177. assert track.error_message == "Datei nicht gefunden"
  178. def test_shortcut_properties(self):
  179. """Testet Kurzschluss-Properties."""
  180. track = Track(
  181. metadata=TrackMetadata(
  182. title="Test Song",
  183. artist="Test Artist",
  184. duration_ms=180000,
  185. )
  186. )
  187. assert track.title == "Test Song"
  188. assert track.artist == "Test Artist"
  189. assert track.duration_ms == 180000
  190. def test_to_dict(self):
  191. """Testet to_dict Konvertierung."""
  192. track = Track(
  193. source_type="local",
  194. path=Path("/music/test.mp3"),
  195. metadata=TrackMetadata(title="Test"),
  196. )
  197. result = track.to_dict()
  198. assert result["id"] == track.id
  199. assert result["source_type"] == "local"
  200. assert result["path"] == "/music/test.mp3"
  201. assert result["metadata"]["title"] == "Test"
  202. # =============================================================================
  203. # Tests für AudioFormat
  204. # =============================================================================
  205. class TestAudioFormat:
  206. """Tests für AudioFormat Enum."""
  207. def test_from_extension(self):
  208. """Testet Format-Erkennung aus Dateiendung."""
  209. assert AudioFormat.from_extension("mp3") == AudioFormat.MP3
  210. assert AudioFormat.from_extension(".MP3") == AudioFormat.MP3
  211. assert AudioFormat.from_extension("wav") == AudioFormat.WAV
  212. assert AudioFormat.from_extension("ogg") == AudioFormat.OGG
  213. assert AudioFormat.from_extension("flac") == AudioFormat.FLAC
  214. assert AudioFormat.from_extension("unknown") == AudioFormat.UNKNOWN
  215. def test_from_mime(self):
  216. """Testet Format-Erkennung aus MIME-Type."""
  217. assert AudioFormat.from_mime("audio/mpeg") == AudioFormat.MP3
  218. assert AudioFormat.from_mime("audio/mp3") == AudioFormat.MP3
  219. assert AudioFormat.from_mime("audio/wav") == AudioFormat.WAV
  220. assert AudioFormat.from_mime("audio/ogg") == AudioFormat.OGG
  221. assert AudioFormat.from_mime("audio/flac") == AudioFormat.FLAC
  222. assert AudioFormat.from_mime("text/html") == AudioFormat.UNKNOWN
  223. # =============================================================================
  224. # Tests für AudioInfo
  225. # =============================================================================
  226. class TestAudioInfo:
  227. """Tests für AudioInfo Datenklasse."""
  228. def test_defaults(self):
  229. """Testet Standard-Werte."""
  230. info = AudioInfo(format=AudioFormat.MP3)
  231. assert info.format == AudioFormat.MP3
  232. assert info.sample_rate == 44100
  233. assert info.channels == 2
  234. assert info.bit_depth == 16
  235. def test_duration_seconds(self):
  236. """Testet duration_seconds Property."""
  237. info = AudioInfo(format=AudioFormat.MP3, duration_ms=180000)
  238. assert info.duration_seconds == 180.0
  239. def test_is_stereo(self):
  240. """Testet is_stereo Property."""
  241. mono = AudioInfo(format=AudioFormat.MP3, channels=1)
  242. stereo = AudioInfo(format=AudioFormat.MP3, channels=2)
  243. assert mono.is_stereo is False
  244. assert stereo.is_stereo is True
  245. # =============================================================================
  246. # Tests für DecodedAudio
  247. # =============================================================================
  248. class TestDecodedAudio:
  249. """Tests für DecodedAudio Datenklasse."""
  250. def test_duration_calculation(self):
  251. """Testet Dauer-Berechnung."""
  252. # 1 Sekunde @ 44100 Hz, Stereo, 16-bit = 44100 * 2 * 2 = 176400 bytes
  253. pcm_data = bytes(176400)
  254. audio = DecodedAudio(
  255. pcm_data=pcm_data,
  256. sample_rate=44100,
  257. channels=2,
  258. bit_depth=16,
  259. )
  260. assert audio.duration_ms == 1000
  261. assert audio.sample_count == 44100
  262. def test_sample_count(self):
  263. """Testet Sample-Zählung."""
  264. # 100 Samples @ Stereo, 16-bit = 100 * 2 * 2 = 400 bytes
  265. pcm_data = bytes(400)
  266. audio = DecodedAudio(
  267. pcm_data=pcm_data,
  268. sample_rate=44100,
  269. channels=2,
  270. bit_depth=16,
  271. )
  272. assert audio.sample_count == 100
  273. # =============================================================================
  274. # Tests für LocalFileSource
  275. # =============================================================================
  276. class TestLocalFileSource:
  277. """Tests für LocalFileSource."""
  278. def test_initialization(self):
  279. """Testet Initialisierung."""
  280. source = LocalFileSource(name="Test Music")
  281. assert source.name == "Test Music"
  282. assert source.source_type == SourceType.LOCAL
  283. assert source.paths == []
  284. assert source.recursive is True
  285. def test_add_path(self, tmp_path):
  286. """Testet Hinzufügen von Pfaden."""
  287. source = LocalFileSource()
  288. # Existierendes Verzeichnis
  289. assert source.add_path(tmp_path) is True
  290. assert tmp_path in source.paths
  291. # Nicht existierendes Verzeichnis
  292. fake_path = tmp_path / "nonexistent"
  293. assert source.add_path(fake_path) is False
  294. def test_remove_path(self, tmp_path):
  295. """Testet Entfernen von Pfaden."""
  296. source = LocalFileSource(paths=[tmp_path])
  297. assert source.remove_path(tmp_path) is True
  298. assert tmp_path not in source.paths
  299. # Nicht vorhandener Pfad
  300. assert source.remove_path(tmp_path) is False
  301. @pytest.mark.asyncio
  302. async def test_connect(self, tmp_path):
  303. """Testet connect."""
  304. source = LocalFileSource(paths=[tmp_path])
  305. result = await source.connect()
  306. assert result is True
  307. assert source.state == SourceState.AVAILABLE
  308. @pytest.mark.asyncio
  309. async def test_connect_no_paths(self):
  310. """Testet connect ohne Pfade."""
  311. source = LocalFileSource()
  312. result = await source.connect()
  313. assert result is False
  314. assert source.state == SourceState.UNAVAILABLE
  315. @pytest.mark.asyncio
  316. async def test_scan_empty_directory(self, tmp_path):
  317. """Testet Scan eines leeren Verzeichnisses."""
  318. source = LocalFileSource(paths=[tmp_path])
  319. count = await source.scan()
  320. assert count == 0
  321. assert source.track_count == 0
  322. @pytest.mark.asyncio
  323. async def test_scan_with_files(self, tmp_path):
  324. """Testet Scan mit Dummy-Dateien."""
  325. # Dummy MP3-Dateien erstellen
  326. (tmp_path / "song1.mp3").write_bytes(b"fake mp3 data")
  327. (tmp_path / "song2.mp3").write_bytes(b"fake mp3 data")
  328. (tmp_path / "not_music.txt").write_bytes(b"text file")
  329. source = LocalFileSource(paths=[tmp_path])
  330. count = await source.scan()
  331. # Sollte nur MP3-Dateien finden
  332. assert count == 2
  333. assert source.track_count == 2
  334. @pytest.mark.asyncio
  335. async def test_scan_recursive(self, tmp_path):
  336. """Testet rekursiven Scan."""
  337. # Unterverzeichnis mit Dateien
  338. subdir = tmp_path / "subdir"
  339. subdir.mkdir()
  340. (subdir / "song.mp3").write_bytes(b"fake mp3")
  341. (tmp_path / "root.mp3").write_bytes(b"fake mp3")
  342. source = LocalFileSource(paths=[tmp_path], recursive=True)
  343. count = await source.scan()
  344. assert count == 2
  345. # Nicht-rekursiv
  346. source2 = LocalFileSource(paths=[tmp_path], recursive=False)
  347. count2 = await source2.scan()
  348. assert count2 == 1
  349. @pytest.mark.asyncio
  350. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  351. async def test_scan_real_music_files(self):
  352. """Testet Scan mit echten Musikdateien."""
  353. source = LocalFileSource(paths=[MUSIC_DIR])
  354. count = await source.scan()
  355. assert count > 0
  356. assert source.track_count > 0
  357. # Prüfe ersten Track
  358. tracks = list(source.tracks)
  359. assert len(tracks) > 0
  360. track = tracks[0]
  361. assert track.path is not None
  362. assert track.path.exists()
  363. assert track.state == TrackState.AVAILABLE
  364. def test_supported_extensions(self):
  365. """Testet unterstützte Dateiendungen."""
  366. assert ".mp3" in SUPPORTED_EXTENSIONS
  367. assert ".wav" in SUPPORTED_EXTENSIONS
  368. assert ".ogg" in SUPPORTED_EXTENSIONS
  369. assert ".flac" in SUPPORTED_EXTENSIONS
  370. # =============================================================================
  371. # Tests für MP3Decoder
  372. # =============================================================================
  373. class TestMP3Decoder:
  374. """Tests für MP3Decoder."""
  375. def test_format(self):
  376. """Testet Format-Konstanten."""
  377. decoder = MP3Decoder()
  378. assert decoder.FORMAT == AudioFormat.MP3
  379. assert "mp3" in decoder.EXTENSIONS
  380. assert "audio/mpeg" in decoder.MIME_TYPES
  381. def test_supports_extension(self):
  382. """Testet Extension-Support."""
  383. decoder = MP3Decoder()
  384. assert decoder.supports_extension("mp3") is True
  385. assert decoder.supports_extension(".mp3") is True
  386. assert decoder.supports_extension("wav") is False
  387. def test_supports_mime(self):
  388. """Testet MIME-Type-Support."""
  389. decoder = MP3Decoder()
  390. assert decoder.supports_mime("audio/mpeg") is True
  391. assert decoder.supports_mime("audio/mp3") is True
  392. assert decoder.supports_mime("audio/wav") is False
  393. @pytest.mark.asyncio
  394. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  395. async def test_get_info_real_file(self):
  396. """Testet Info-Extraktion aus echter Datei."""
  397. mp3_file = get_first_mp3()
  398. if mp3_file is None:
  399. pytest.skip("Keine MP3-Datei gefunden")
  400. decoder = MP3Decoder()
  401. # Sollte auch ohne Mutagen funktionieren (Header-Parsing)
  402. info = await decoder.get_info(mp3_file)
  403. assert info.format == AudioFormat.MP3
  404. assert info.sample_rate > 0
  405. assert info.channels in (1, 2)
  406. assert info.file_size > 0
  407. @pytest.mark.asyncio
  408. @pytest.mark.skipif(not (has_pydub() or has_ffmpeg()), reason="Kein Decoder verfügbar")
  409. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  410. async def test_decode_real_file(self):
  411. """Testet Dekodierung einer echten Datei."""
  412. mp3_file = get_first_mp3()
  413. if mp3_file is None:
  414. pytest.skip("Keine MP3-Datei gefunden")
  415. decoder = MP3Decoder()
  416. if not decoder.is_available:
  417. pytest.skip("Decoder nicht verfügbar")
  418. decoded = await decoder.decode(mp3_file)
  419. assert len(decoded.pcm_data) > 0
  420. assert decoded.sample_rate == 44100 # Standard Output
  421. assert decoded.channels == 2
  422. assert decoded.bit_depth == 16
  423. @pytest.mark.asyncio
  424. async def test_decode_without_backend(self, tmp_path):
  425. """Testet Fehler ohne Backend."""
  426. # Fake MP3-Datei
  427. fake_mp3 = tmp_path / "fake.mp3"
  428. fake_mp3.write_bytes(b"not a real mp3")
  429. decoder = MP3Decoder()
  430. if decoder.is_available:
  431. pytest.skip("Decoder ist verfügbar")
  432. with pytest.raises(DecoderError, match="Kein MP3-Decoder verfügbar"):
  433. await decoder.decode(fake_mp3)
  434. # =============================================================================
  435. # Tests für DecoderRegistry
  436. # =============================================================================
  437. class TestDecoderRegistry:
  438. """Tests für DecoderRegistry."""
  439. def test_get_decoder_for_extension(self):
  440. """Testet Decoder-Lookup nach Extension."""
  441. registry = DecoderRegistry()
  442. registry.register_default_decoders()
  443. # WAV sollte immer verfügbar sein
  444. decoder = registry.get_decoder_for_extension("wav")
  445. assert decoder is not None
  446. assert decoder.FORMAT == AudioFormat.WAV
  447. # Unbekannte Extension
  448. decoder = registry.get_decoder_for_extension("unknown")
  449. assert decoder is None
  450. def test_get_decoder_for_format(self):
  451. """Testet Decoder-Lookup nach Format."""
  452. registry = DecoderRegistry()
  453. registry.register_default_decoders()
  454. # WAV sollte immer verfügbar sein
  455. decoder = registry.get_decoder(AudioFormat.WAV)
  456. assert decoder is not None
  457. def test_mp3_decoder_if_available(self):
  458. """Testet MP3-Decoder wenn verfügbar."""
  459. registry = DecoderRegistry()
  460. registry.register_default_decoders()
  461. decoder = registry.get_decoder_for_extension("mp3")
  462. if has_pydub() or has_ffmpeg():
  463. assert decoder is not None
  464. assert isinstance(decoder, MP3Decoder)
  465. else:
  466. # MP3-Decoder nicht verfügbar ohne pydub/ffmpeg
  467. # Das ist erwartetes Verhalten
  468. pass
  469. def test_extensions(self):
  470. """Testet Liste unterstützter Extensions."""
  471. registry = DecoderRegistry()
  472. registry.register_default_decoders()
  473. extensions = registry.extensions
  474. # WAV sollte immer verfügbar sein
  475. assert "wav" in extensions or "wave" in extensions
  476. # =============================================================================
  477. # Tests für StreamConfig
  478. # =============================================================================
  479. class TestStreamConfig:
  480. """Tests für StreamConfig."""
  481. def test_for_music(self):
  482. """Testet Musik-Konfiguration."""
  483. config = StreamConfig.for_music()
  484. assert config.sample_rate == 48000
  485. assert config.channels == 2
  486. assert config.bit_depth == 16
  487. def test_for_voice(self):
  488. """Testet Voice-Konfiguration."""
  489. config = StreamConfig.for_voice()
  490. assert config.sample_rate == 16000
  491. assert config.channels == 1
  492. assert config.bit_depth == 16
  493. def test_bytes_per_second(self):
  494. """Testet bytes_per_second Berechnung."""
  495. config = StreamConfig(
  496. sample_rate=44100,
  497. channels=2,
  498. bit_depth=16,
  499. )
  500. # 44100 * 2 * 2 = 176400
  501. assert config.bytes_per_second == 176400
  502. # =============================================================================
  503. # Tests für StreamStats
  504. # =============================================================================
  505. class TestStreamStats:
  506. """Tests für StreamStats."""
  507. def test_initial_values(self):
  508. """Testet Initialwerte."""
  509. stats = StreamStats()
  510. assert stats.bytes_sent == 0
  511. assert stats.packets_sent == 0
  512. assert stats.packets_lost == 0
  513. assert stats.loss_rate == 0.0
  514. def test_loss_rate(self):
  515. """Testet Loss-Rate-Berechnung."""
  516. stats = StreamStats(packets_sent=90, packets_lost=10)
  517. assert stats.loss_rate == 0.1 # 10%
  518. def test_add_latency_sample(self):
  519. """Testet Latenz-Tracking."""
  520. stats = StreamStats()
  521. stats.add_latency_sample(10.0)
  522. stats.add_latency_sample(20.0)
  523. stats.add_latency_sample(30.0)
  524. assert stats.min_latency_ms == 10.0
  525. assert stats.max_latency_ms == 30.0
  526. assert stats.avg_latency_ms == 20.0
  527. def test_to_dict(self):
  528. """Testet to_dict Konvertierung."""
  529. stats = StreamStats(bytes_sent=1000, packets_sent=10)
  530. result = stats.to_dict()
  531. assert result["bytes_sent"] == 1000
  532. assert result["packets_sent"] == 10
  533. assert "latency" in result
  534. # =============================================================================
  535. # Tests für StreamSession
  536. # =============================================================================
  537. class TestStreamSession:
  538. """Tests für StreamSession."""
  539. def test_initialization(self):
  540. """Testet Initialisierung."""
  541. session = StreamSession(satellite_id="sat_001")
  542. assert session.satellite_id == "sat_001"
  543. assert session.state == StreamState.IDLE
  544. assert session.is_streaming is False
  545. assert session.position_ms == 0
  546. @pytest.mark.asyncio
  547. async def test_start_and_stop(self):
  548. """Testet Start und Stop."""
  549. session = StreamSession(satellite_id="sat_001")
  550. track = Track(metadata=TrackMetadata(title="Test", duration_ms=180000))
  551. success = await session.start(track)
  552. assert success is True
  553. assert session.current_track == track
  554. assert session.state in (StreamState.BUFFERING, StreamState.CONNECTING)
  555. await session.stop()
  556. assert session.state == StreamState.IDLE
  557. assert session.current_track is None
  558. @pytest.mark.asyncio
  559. async def test_pause_resume(self):
  560. """Testet Pause und Resume."""
  561. send_callback = AsyncMock(return_value=True)
  562. session = StreamSession(satellite_id="sat_001", send_callback=send_callback)
  563. track = Track(metadata=TrackMetadata(title="Test", duration_ms=180000))
  564. await session.start(track)
  565. # Füttere genug Daten um zu STREAMING zu wechseln
  566. data = bytes(session.config.prebuffer_bytes)
  567. await session.feed(data)
  568. # Warte kurz auf State-Update
  569. await asyncio.sleep(0.1)
  570. if session.state == StreamState.STREAMING:
  571. await session.pause()
  572. assert session.state == StreamState.PAUSED
  573. await session.resume()
  574. assert session.state == StreamState.STREAMING
  575. await session.stop()
  576. @pytest.mark.asyncio
  577. async def test_feed_data(self):
  578. """Testet Daten-Fütterung."""
  579. session = StreamSession(satellite_id="sat_001")
  580. track = Track(metadata=TrackMetadata(duration_ms=10000))
  581. await session.start(track)
  582. # Kleine Daten einfügen
  583. data = bytes(1000)
  584. result = await session.feed(data)
  585. assert result is True
  586. assert session.buffer_ms > 0
  587. await session.stop()
  588. @pytest.mark.asyncio
  589. async def test_buffer_overflow(self):
  590. """Testet Puffer-Überlauf."""
  591. config = StreamConfig(max_buffer_ms=100) # Sehr kleiner Puffer
  592. session = StreamSession(satellite_id="sat_001", config=config)
  593. track = Track(metadata=TrackMetadata(duration_ms=10000))
  594. await session.start(track)
  595. # Versuche mehr Daten einzufügen als der Puffer fasst
  596. large_data = bytes(config.max_buffer_ms * config.bytes_per_second)
  597. result1 = await session.feed(large_data)
  598. result2 = await session.feed(large_data)
  599. # Zweites Einfügen sollte abgelehnt werden
  600. assert result2 is False
  601. assert session.stats.buffer_overruns > 0
  602. await session.stop()
  603. @pytest.mark.asyncio
  604. async def test_seek(self):
  605. """Testet Seek-Funktion."""
  606. session = StreamSession(satellite_id="sat_001")
  607. track = Track(metadata=TrackMetadata(duration_ms=180000))
  608. await session.start(track)
  609. result = await session.seek(90000)
  610. assert result is True
  611. assert session.position_ms == 90000
  612. assert session.state == StreamState.BUFFERING # Puffer geleert
  613. await session.stop()
  614. def test_get_status(self):
  615. """Testet Status-Abfrage."""
  616. session = StreamSession(satellite_id="sat_001")
  617. status = session.get_status()
  618. assert status["satellite_id"] == "sat_001"
  619. assert status["state"] == "idle"
  620. assert "position_ms" in status
  621. assert "buffer_level" in status
  622. # =============================================================================
  623. # Tests für AudioStreamer
  624. # =============================================================================
  625. class TestAudioStreamer:
  626. """Tests für AudioStreamer."""
  627. def test_initialization(self):
  628. """Testet Initialisierung."""
  629. streamer = AudioStreamer()
  630. assert streamer.session_count == 0
  631. assert streamer.is_streaming is False
  632. assert streamer.current_track is None
  633. def test_add_satellite(self):
  634. """Testet Hinzufügen von Satellites."""
  635. streamer = AudioStreamer()
  636. callback = AsyncMock(return_value=True)
  637. session = streamer.add_satellite("sat_001", callback)
  638. assert session is not None
  639. assert "sat_001" in streamer
  640. assert streamer.session_count == 1
  641. @pytest.mark.asyncio
  642. async def test_remove_satellite(self):
  643. """Testet Entfernen von Satellites."""
  644. streamer = AudioStreamer()
  645. callback = AsyncMock(return_value=True)
  646. streamer.add_satellite("sat_001", callback)
  647. result = await streamer.remove_satellite("sat_001")
  648. assert result is True
  649. assert "sat_001" not in streamer
  650. assert streamer.session_count == 0
  651. @pytest.mark.asyncio
  652. async def test_start_streaming(self):
  653. """Testet Streaming-Start."""
  654. streamer = AudioStreamer()
  655. callback = AsyncMock(return_value=True)
  656. streamer.add_satellite("sat_001", callback)
  657. track = Track(metadata=TrackMetadata(title="Test", duration_ms=180000))
  658. results = await streamer.start(track)
  659. assert results["sat_001"] is True
  660. assert streamer.current_track == track
  661. await streamer.stop()
  662. @pytest.mark.asyncio
  663. async def test_feed_multiple_satellites(self):
  664. """Testet Daten-Fütterung an mehrere Satellites."""
  665. streamer = AudioStreamer()
  666. callbacks = [AsyncMock(return_value=True) for _ in range(3)]
  667. for i, cb in enumerate(callbacks):
  668. streamer.add_satellite(f"sat_{i}", cb)
  669. track = Track(metadata=TrackMetadata(duration_ms=10000))
  670. await streamer.start(track)
  671. data = bytes(1000)
  672. results = await streamer.feed(data)
  673. assert len(results) == 3
  674. assert all(results.values())
  675. await streamer.stop()
  676. @pytest.mark.asyncio
  677. async def test_pause_resume_all(self):
  678. """Testet Pause/Resume für alle Sessions."""
  679. streamer = AudioStreamer()
  680. callback = AsyncMock(return_value=True)
  681. streamer.add_satellite("sat_001", callback)
  682. track = Track(metadata=TrackMetadata(duration_ms=180000))
  683. await streamer.start(track)
  684. await streamer.pause()
  685. session = streamer.get_session("sat_001")
  686. # Session wechselt zu PAUSED nur wenn sie STREAMING war
  687. await streamer.resume()
  688. # Session sollte wieder aktiv sein
  689. await streamer.stop()
  690. def test_get_status(self):
  691. """Testet Status-Abfrage."""
  692. streamer = AudioStreamer()
  693. callback = AsyncMock(return_value=True)
  694. streamer.add_satellite("sat_001", callback)
  695. status = streamer.get_status()
  696. assert status["session_count"] == 1
  697. assert "sat_001" in status["sessions"]
  698. # =============================================================================
  699. # Tests für PlayQueue
  700. # =============================================================================
  701. class TestPlayQueue:
  702. """Tests für PlayQueue."""
  703. def test_initialization(self):
  704. """Testet Initialisierung."""
  705. queue = PlayQueue()
  706. assert queue.count == 0
  707. assert queue.is_empty is True
  708. assert queue.has_next is False
  709. assert queue.state == QueueState.IDLE
  710. def test_add_track(self):
  711. """Testet Hinzufügen von Tracks."""
  712. queue = PlayQueue()
  713. track = Track(metadata=TrackMetadata(title="Test"))
  714. item = queue.add(track)
  715. assert item is not None
  716. assert queue.count == 1
  717. assert queue.has_next is True
  718. def test_add_play_next(self):
  719. """Testet Play-Next-Funktion."""
  720. queue = PlayQueue()
  721. track1 = Track(id="t1", metadata=TrackMetadata(title="First"))
  722. track2 = Track(id="t2", metadata=TrackMetadata(title="Second"))
  723. track3 = Track(id="t3", metadata=TrackMetadata(title="Play Next"))
  724. queue.add(track1)
  725. queue.add(track2)
  726. queue.add(track3, play_next=True)
  727. # Play-Next sollte am Anfang sein
  728. assert queue.items[0].track.id == "t3"
  729. assert queue.items[0].priority is True
  730. def test_remove_by_index(self):
  731. """Testet Entfernen nach Index."""
  732. queue = PlayQueue()
  733. queue.add(Track(id="t1"))
  734. queue.add(Track(id="t2"))
  735. item = queue.remove(0)
  736. assert item is not None
  737. assert item.track.id == "t1"
  738. assert queue.count == 1
  739. def test_remove_by_id(self):
  740. """Testet Entfernen nach ID."""
  741. queue = PlayQueue()
  742. item = queue.add(Track(id="t1"))
  743. removed = queue.remove_by_id(item.id)
  744. assert removed is not None
  745. assert queue.count == 0
  746. def test_next(self):
  747. """Testet next() Navigation."""
  748. queue = PlayQueue()
  749. queue.add(Track(id="t1"))
  750. queue.add(Track(id="t2"))
  751. item = queue.next()
  752. assert item is not None
  753. assert item.track.id == "t1"
  754. assert queue.current_item == item
  755. assert queue.has_previous is False # Noch keine History
  756. # Nächster Track
  757. item2 = queue.next()
  758. assert item2.track.id == "t2"
  759. assert queue.has_previous is True # Jetzt gibt es History
  760. def test_previous(self):
  761. """Testet previous() Navigation."""
  762. queue = PlayQueue()
  763. queue.add(Track(id="t1"))
  764. queue.add(Track(id="t2"))
  765. queue.next() # t1
  766. queue.next() # t2
  767. prev = queue.previous()
  768. assert prev is not None
  769. assert prev.track.id == "t1"
  770. assert queue.current_item == prev
  771. def test_clear(self):
  772. """Testet Queue leeren."""
  773. queue = PlayQueue()
  774. queue.add(Track(id="t1"))
  775. queue.add(Track(id="t2"))
  776. queue.next() # Setzt current
  777. count = queue.clear()
  778. assert count == 1 # Nur der verbleibende Track
  779. assert queue.is_empty is True
  780. assert queue.current_item is not None # Current bleibt
  781. def test_clear_all(self):
  782. """Testet komplettes Leeren."""
  783. queue = PlayQueue()
  784. queue.add(Track(id="t1"))
  785. queue.next()
  786. count = queue.clear_all()
  787. assert queue.current_item is None
  788. assert queue.state == QueueState.IDLE
  789. def test_total_duration(self):
  790. """Testet Gesamt-Dauer-Berechnung."""
  791. queue = PlayQueue()
  792. queue.add(Track(metadata=TrackMetadata(duration_ms=60000)))
  793. queue.add(Track(metadata=TrackMetadata(duration_ms=120000)))
  794. assert queue.total_duration_ms == 180000
  795. def test_move(self):
  796. """Testet Verschieben von Items."""
  797. queue = PlayQueue()
  798. queue.add(Track(id="t1"))
  799. queue.add(Track(id="t2"))
  800. queue.add(Track(id="t3"))
  801. result = queue.move(2, 0)
  802. assert result is True
  803. assert queue.items[0].track.id == "t3"
  804. def test_peek(self):
  805. """Testet Vorschau ohne Entfernen."""
  806. queue = PlayQueue()
  807. queue.add(Track(id="t1"))
  808. queue.add(Track(id="t2"))
  809. items = queue.peek(2)
  810. assert len(items) == 2
  811. assert queue.count == 2 # Nichts entfernt
  812. def test_peek_next(self):
  813. """Testet peek_next."""
  814. queue = PlayQueue()
  815. assert queue.peek_next() is None
  816. queue.add(Track(id="t1"))
  817. item = queue.peek_next()
  818. assert item is not None
  819. assert item.track.id == "t1"
  820. def test_find_track(self):
  821. """Testet Track-Suche."""
  822. queue = PlayQueue()
  823. queue.add(Track(id="t1"))
  824. queue.add(Track(id="t2"))
  825. result = queue.find_track("t2")
  826. assert result is not None
  827. index, item = result
  828. assert index == 1
  829. assert item.track.id == "t2"
  830. def test_contains_track(self):
  831. """Testet contains_track."""
  832. queue = PlayQueue()
  833. queue.add(Track(id="t1"))
  834. assert queue.contains_track("t1") is True
  835. assert queue.contains_track("t2") is False
  836. def test_state_management(self):
  837. """Testet Zustands-Management."""
  838. queue = PlayQueue()
  839. queue.add(Track(id="t1"))
  840. queue.next()
  841. queue.set_playing()
  842. assert queue.state == QueueState.PLAYING
  843. queue.set_paused()
  844. assert queue.state == QueueState.PAUSED
  845. queue.set_loading()
  846. assert queue.state == QueueState.LOADING
  847. def test_to_dict(self):
  848. """Testet to_dict Konvertierung."""
  849. queue = PlayQueue()
  850. queue.add(Track(id="t1", metadata=TrackMetadata(title="Test")))
  851. result = queue.to_dict()
  852. assert result["count"] == 1
  853. assert len(result["items"]) == 1
  854. assert result["items"][0]["track"]["id"] == "t1"
  855. # =============================================================================
  856. # Tests für Playlist
  857. # =============================================================================
  858. class TestPlaylist:
  859. """Tests für Playlist."""
  860. def test_initialization(self):
  861. """Testet Initialisierung."""
  862. playlist = Playlist(name="Test Playlist")
  863. assert playlist.name == "Test Playlist"
  864. assert playlist.count == 0
  865. assert playlist.is_empty is True
  866. def test_add_track(self):
  867. """Testet Hinzufügen von Tracks."""
  868. playlist = Playlist(name="Test")
  869. track = Track(metadata=TrackMetadata(title="Song"))
  870. playlist.add(track)
  871. assert playlist.count == 1
  872. def test_remove_track(self):
  873. """Testet Entfernen von Tracks."""
  874. playlist = Playlist(name="Test")
  875. track = Track(id="t1")
  876. playlist.add(track)
  877. result = playlist.remove(0)
  878. assert result is not None
  879. assert playlist.count == 0
  880. def test_next_sequential(self):
  881. """Testet sequentielle Navigation."""
  882. playlist = Playlist(name="Test", shuffle_mode=ShuffleMode.OFF)
  883. playlist.add(Track(id="t1"))
  884. playlist.add(Track(id="t2"))
  885. playlist.add(Track(id="t3"))
  886. track1 = playlist.next()
  887. assert track1.id == "t1"
  888. track2 = playlist.next()
  889. assert track2.id == "t2"
  890. track3 = playlist.next()
  891. assert track3.id == "t3"
  892. def test_previous(self):
  893. """Testet Zurück-Navigation."""
  894. playlist = Playlist(name="Test")
  895. playlist.add(Track(id="t1"))
  896. playlist.add(Track(id="t2"))
  897. playlist.next() # t1
  898. playlist.next() # t2
  899. prev = playlist.previous()
  900. assert prev.id == "t1"
  901. def test_repeat_off(self):
  902. """Testet Repeat-Modus OFF."""
  903. playlist = Playlist(name="Test", repeat_mode=RepeatMode.OFF)
  904. playlist.add(Track(id="t1"))
  905. playlist.next() # t1
  906. track = playlist.next() # Ende
  907. assert track is None
  908. def test_repeat_all(self):
  909. """Testet Repeat-Modus ALL."""
  910. playlist = Playlist(name="Test", repeat_mode=RepeatMode.ALL)
  911. playlist.add(Track(id="t1"))
  912. playlist.add(Track(id="t2"))
  913. playlist.next() # t1
  914. playlist.next() # t2
  915. track = playlist.next() # Zurück zum Anfang
  916. assert track.id == "t1"
  917. def test_repeat_one(self):
  918. """Testet Repeat-Modus ONE."""
  919. playlist = Playlist(name="Test", repeat_mode=RepeatMode.ONE)
  920. playlist.add(Track(id="t1"))
  921. playlist.add(Track(id="t2"))
  922. # Bei REPEAT_ONE bleibt next() beim aktuellen Track
  923. # Zuerst müssen wir zum ersten Track navigieren
  924. playlist.jump_to(0)
  925. assert playlist.current_track.id == "t1"
  926. track = playlist.next() # Sollte wieder t1 sein
  927. assert track.id == "t1"
  928. track = playlist.next() # Immer noch t1
  929. assert track.id == "t1"
  930. def test_shuffle(self):
  931. """Testet Shuffle-Modus."""
  932. playlist = Playlist(name="Test", shuffle_mode=ShuffleMode.ON)
  933. for i in range(10):
  934. playlist.add(Track(id=f"t{i}"))
  935. # Mehrfach durch die Playlist navigieren
  936. # Bei Shuffle sollte die Reihenfolge zufällig sein
  937. order = []
  938. for _ in range(10):
  939. track = playlist.next()
  940. if track:
  941. order.append(track.id)
  942. # Bei Shuffle sollten alle Tracks vorkommen (ohne Repeat)
  943. assert len(set(order)) == len(order) # Keine Duplikate
  944. def test_jump_to(self):
  945. """Testet Sprung zu Index."""
  946. playlist = Playlist(name="Test")
  947. playlist.add(Track(id="t1"))
  948. playlist.add(Track(id="t2"))
  949. playlist.add(Track(id="t3"))
  950. track = playlist.jump_to(2)
  951. assert track.id == "t3"
  952. assert playlist.current_index == 2
  953. def test_clear(self):
  954. """Testet Playlist leeren."""
  955. playlist = Playlist(name="Test")
  956. playlist.add(Track(id="t1"))
  957. playlist.add(Track(id="t2"))
  958. playlist.clear()
  959. assert playlist.count == 0
  960. assert playlist.is_empty is True
  961. def test_total_duration(self):
  962. """Testet Gesamt-Dauer."""
  963. playlist = Playlist(name="Test")
  964. playlist.add(Track(metadata=TrackMetadata(duration_ms=60000)))
  965. playlist.add(Track(metadata=TrackMetadata(duration_ms=120000)))
  966. assert playlist.total_duration_ms == 180000
  967. def test_to_dict(self):
  968. """Testet to_dict Konvertierung."""
  969. playlist = Playlist(name="Test Playlist")
  970. playlist.add(Track(id="t1"))
  971. result = playlist.to_dict()
  972. assert result["name"] == "Test Playlist"
  973. assert result["count"] == 1
  974. assert len(result["items"]) == 1
  975. def test_peek_next(self):
  976. """Testet peek_next."""
  977. playlist = Playlist(name="Test", shuffle_mode=ShuffleMode.OFF)
  978. playlist.add(Track(id="t1"))
  979. playlist.add(Track(id="t2"))
  980. # Am Anfang - current_index ist -1, also ist nächster Track t1
  981. next_track = playlist.peek_next()
  982. assert next_track.id == "t1"
  983. # Nach einem next()
  984. playlist.next()
  985. next_track = playlist.peek_next()
  986. assert next_track.id == "t2"
  987. # =============================================================================
  988. # Integrationstests
  989. # =============================================================================
  990. class TestMusicIntegration:
  991. """Integrationstests für das Musik-System."""
  992. @pytest.mark.asyncio
  993. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  994. async def test_scan_and_queue(self):
  995. """Testet Scannen und Einreihen von Tracks."""
  996. # Musikdateien scannen
  997. source = LocalFileSource(paths=[MUSIC_DIR])
  998. await source.scan()
  999. assert source.track_count > 0
  1000. # Tracks in Queue einfügen
  1001. queue = PlayQueue()
  1002. for track in source.tracks:
  1003. queue.add(track)
  1004. assert queue.count == source.track_count
  1005. # Durch Queue navigieren
  1006. first = queue.next()
  1007. assert first is not None
  1008. assert first.track.path.exists()
  1009. @pytest.mark.asyncio
  1010. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  1011. async def test_scan_and_playlist(self):
  1012. """Testet Scannen und Playlist-Erstellung."""
  1013. # Musikdateien scannen
  1014. source = LocalFileSource(paths=[MUSIC_DIR])
  1015. await source.scan()
  1016. # Playlist erstellen
  1017. playlist = Playlist(name="Test Mix")
  1018. for track in source.tracks:
  1019. playlist.add(track)
  1020. assert playlist.count == source.track_count
  1021. # Shuffle aktivieren und durchspielen
  1022. playlist.set_shuffle(ShuffleMode.ON)
  1023. played = []
  1024. # start() gibt den ersten Track zurück
  1025. first = playlist.start()
  1026. if first:
  1027. played.append(first.id)
  1028. # Dann mit next() fortfahren
  1029. while True:
  1030. track = playlist.next()
  1031. if track is None:
  1032. break
  1033. played.append(track.id)
  1034. assert len(played) == source.track_count
  1035. @pytest.mark.asyncio
  1036. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  1037. async def test_full_streaming_pipeline(self):
  1038. """Testet die komplette Streaming-Pipeline."""
  1039. # Scan
  1040. source = LocalFileSource(paths=[MUSIC_DIR])
  1041. await source.scan()
  1042. track = list(source.tracks)[0]
  1043. # Streamer einrichten
  1044. streamer = AudioStreamer()
  1045. received_data = []
  1046. async def receive_callback(data: bytes) -> bool:
  1047. received_data.append(data)
  1048. return True
  1049. streamer.add_satellite("test_client", receive_callback)
  1050. # Streaming starten
  1051. await streamer.start(track)
  1052. # Daten füttern
  1053. test_data = bytes(4096)
  1054. await streamer.feed(test_data)
  1055. # Kurz warten für Streaming-Loop
  1056. await asyncio.sleep(0.2)
  1057. # Stoppen
  1058. await streamer.stop()
  1059. # Prüfen ob Daten empfangen wurden
  1060. # (Hängt davon ab ob genug gepuffert war)
  1061. session = streamer.get_session("test_client")
  1062. assert session is None or session.stats.bytes_sent >= 0
  1063. # =============================================================================
  1064. # Performance Tests
  1065. # =============================================================================
  1066. class TestMusicPerformance:
  1067. """Performance-Tests für das Musik-System."""
  1068. @pytest.mark.asyncio
  1069. @pytest.mark.skipif(not has_music_files(), reason="Keine Musikdateien vorhanden")
  1070. async def test_scan_performance(self):
  1071. """Testet Scan-Performance."""
  1072. import time
  1073. source = LocalFileSource(paths=[MUSIC_DIR])
  1074. start = time.monotonic()
  1075. count = await source.scan()
  1076. duration = time.monotonic() - start
  1077. # Sollte schnell sein (< 1s für übliche Verzeichnisse)
  1078. assert duration < 5.0
  1079. assert count > 0
  1080. def test_queue_performance(self):
  1081. """Testet Queue-Performance mit vielen Tracks."""
  1082. import time
  1083. queue = PlayQueue()
  1084. # 1000 Tracks hinzufügen
  1085. start = time.monotonic()
  1086. for i in range(1000):
  1087. queue.add(Track(id=f"track_{i}"))
  1088. add_duration = time.monotonic() - start
  1089. assert add_duration < 1.0 # Sollte < 1s sein
  1090. # Durch alle navigieren
  1091. start = time.monotonic()
  1092. while queue.has_next:
  1093. queue.next()
  1094. nav_duration = time.monotonic() - start
  1095. assert nav_duration < 1.0 # Sollte < 1s sein
  1096. def test_playlist_shuffle_performance(self):
  1097. """Testet Shuffle-Performance."""
  1098. import time
  1099. playlist = Playlist(name="Big Playlist", shuffle_mode=ShuffleMode.ON)
  1100. # 1000 Tracks
  1101. for i in range(1000):
  1102. playlist.add(Track(id=f"track_{i}"))
  1103. # Shuffle und durchspielen mit start() + next()
  1104. start = time.monotonic()
  1105. count = 0
  1106. # start() gibt den ersten Track
  1107. if playlist.start() is not None:
  1108. count = 1
  1109. while playlist.next() is not None:
  1110. count += 1
  1111. if count > 1000:
  1112. break # Sicherheit gegen Endlosschleife
  1113. duration = time.monotonic() - start
  1114. assert duration < 2.0
  1115. assert count == 1000