test_assets.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für das Assets-Management-System.
  4. Testet:
  5. - AssetManager: Pfad-Auflösung, Fallback, Caching
  6. - AssetService: ServiceContainer-Integration
  7. - AssetInfo: Metadaten und Eigenschaften
  8. - AssetType: Typ-Erkennung
  9. """
  10. import pytest
  11. import tempfile
  12. import time
  13. from pathlib import Path
  14. from unittest.mock import Mock, AsyncMock, patch
  15. from trixy_core.assets import (
  16. AssetManager,
  17. AssetService,
  18. AssetType,
  19. AssetInfo,
  20. AssetNotFoundError,
  21. )
  22. # =============================================================================
  23. # Fixtures
  24. # =============================================================================
  25. @pytest.fixture
  26. def temp_assets_dir(tmp_path):
  27. """Erstellt ein temporäres Assets-Verzeichnis mit Test-Struktur."""
  28. # Default-Profil
  29. default_dir = tmp_path / "default"
  30. (default_dir / "audio").mkdir(parents=True)
  31. (default_dir / "music").mkdir(parents=True)
  32. (default_dir / "images").mkdir(parents=True)
  33. # Default-Assets erstellen
  34. (default_dir / "audio" / "success.wav").write_bytes(b"RIFF" + b"\x00" * 100)
  35. (default_dir / "audio" / "error.wav").write_bytes(b"RIFF" + b"\x00" * 50)
  36. (default_dir / "music" / "song1.mp3").write_bytes(b"ID3" + b"\x00" * 200)
  37. (default_dir / "images" / "logo.png").write_bytes(b"\x89PNG" + b"\x00" * 100)
  38. (default_dir / "config.json").write_text('{"key": "value"}')
  39. # Custom-Profil
  40. custom_dir = tmp_path / "custom"
  41. (custom_dir / "audio").mkdir(parents=True)
  42. # Custom-Asset (überschreibt default)
  43. (custom_dir / "audio" / "success.wav").write_bytes(b"RIFF" + b"\x00" * 150)
  44. # Zusätzliches Custom-Asset
  45. (custom_dir / "audio" / "custom_sound.wav").write_bytes(b"RIFF" + b"\x00" * 75)
  46. return tmp_path
  47. @pytest.fixture
  48. def asset_manager(temp_assets_dir):
  49. """Erstellt einen AssetManager mit Test-Verzeichnis."""
  50. return AssetManager(
  51. base_dir=temp_assets_dir,
  52. profile="custom",
  53. cache_enabled=True,
  54. cache_ttl=60.0,
  55. )
  56. @pytest.fixture
  57. def mock_application():
  58. """Erstellt eine Mock-Application für AssetService-Tests."""
  59. app = Mock()
  60. app.config = Mock()
  61. app.config.assets_directory = "assets"
  62. app.config.profile = "default"
  63. return app
  64. # =============================================================================
  65. # AssetType Tests
  66. # =============================================================================
  67. class TestAssetType:
  68. """Tests für AssetType-Enum."""
  69. def test_audio_type(self):
  70. """Audio-Typ existiert."""
  71. assert AssetType.AUDIO is not None
  72. assert AssetType.AUDIO.name == "AUDIO"
  73. def test_music_type(self):
  74. """Musik-Typ existiert."""
  75. assert AssetType.MUSIC is not None
  76. def test_image_type(self):
  77. """Bild-Typ existiert."""
  78. assert AssetType.IMAGE is not None
  79. def test_all_types(self):
  80. """Alle Typen sind definiert."""
  81. expected_types = ["AUDIO", "MUSIC", "IMAGE", "VIDEO", "CONFIG", "DATA", "MODEL", "UNKNOWN"]
  82. for type_name in expected_types:
  83. assert hasattr(AssetType, type_name)
  84. # =============================================================================
  85. # AssetInfo Tests
  86. # =============================================================================
  87. class TestAssetInfo:
  88. """Tests für AssetInfo-Dataclass."""
  89. def test_basic_properties(self, temp_assets_dir):
  90. """Grundlegende Eigenschaften werden korrekt gesetzt."""
  91. path = temp_assets_dir / "default" / "audio" / "success.wav"
  92. info = AssetInfo(
  93. path=path,
  94. relative_path="audio/success.wav",
  95. profile="default",
  96. is_default=True,
  97. asset_type=AssetType.AUDIO,
  98. size=104,
  99. modified=time.time(),
  100. )
  101. assert info.path == path
  102. assert info.relative_path == "audio/success.wav"
  103. assert info.profile == "default"
  104. assert info.is_default is True
  105. assert info.asset_type == AssetType.AUDIO
  106. assert info.size == 104
  107. def test_extension_property(self, temp_assets_dir):
  108. """Extension-Property funktioniert."""
  109. path = temp_assets_dir / "default" / "audio" / "success.wav"
  110. info = AssetInfo(
  111. path=path,
  112. relative_path="audio/success.wav",
  113. profile="default",
  114. is_default=True,
  115. asset_type=AssetType.AUDIO,
  116. size=100,
  117. modified=time.time(),
  118. )
  119. assert info.extension == ".wav"
  120. def test_name_property(self, temp_assets_dir):
  121. """Name-Property funktioniert."""
  122. path = temp_assets_dir / "default" / "audio" / "success.wav"
  123. info = AssetInfo(
  124. path=path,
  125. relative_path="audio/success.wav",
  126. profile="default",
  127. is_default=True,
  128. asset_type=AssetType.AUDIO,
  129. size=100,
  130. modified=time.time(),
  131. )
  132. assert info.name == "success.wav"
  133. assert info.stem == "success"
  134. def test_exists_property(self, temp_assets_dir):
  135. """Exists-Property prüft Datei-Existenz."""
  136. path = temp_assets_dir / "default" / "audio" / "success.wav"
  137. info = AssetInfo(
  138. path=path,
  139. relative_path="audio/success.wav",
  140. profile="default",
  141. is_default=True,
  142. asset_type=AssetType.AUDIO,
  143. size=100,
  144. modified=time.time(),
  145. )
  146. assert info.exists is True
  147. # Datei löschen
  148. path.unlink()
  149. assert info.exists is False
  150. def test_checksum_calculation(self, temp_assets_dir):
  151. """Checksum wird korrekt berechnet."""
  152. path = temp_assets_dir / "default" / "audio" / "success.wav"
  153. info = AssetInfo(
  154. path=path,
  155. relative_path="audio/success.wav",
  156. profile="default",
  157. is_default=True,
  158. asset_type=AssetType.AUDIO,
  159. size=104,
  160. modified=time.time(),
  161. )
  162. checksum = info.checksum
  163. assert len(checksum) == 32 # MD5 hex length
  164. # Zweiter Aufruf sollte gecached sein
  165. assert info.checksum == checksum
  166. # =============================================================================
  167. # AssetManager Tests
  168. # =============================================================================
  169. class TestAssetManagerInit:
  170. """Tests für AssetManager-Initialisierung."""
  171. def test_init_creates_directories(self, tmp_path):
  172. """Initialisierung erstellt Verzeichnisse."""
  173. manager = AssetManager(
  174. base_dir=tmp_path / "assets",
  175. profile="test_profile",
  176. )
  177. assert (tmp_path / "assets" / "default").exists()
  178. assert (tmp_path / "assets" / "test_profile").exists()
  179. def test_init_default_values(self, tmp_path):
  180. """Standard-Werte werden gesetzt."""
  181. manager = AssetManager(base_dir=tmp_path)
  182. assert manager.profile == "default"
  183. assert manager._cache_enabled is True
  184. assert manager._cache_ttl == 60.0
  185. def test_base_dir_property(self, asset_manager, temp_assets_dir):
  186. """Base-Dir Property funktioniert."""
  187. assert asset_manager.base_dir == temp_assets_dir
  188. def test_profile_property(self, asset_manager):
  189. """Profile Property funktioniert."""
  190. assert asset_manager.profile == "custom"
  191. def test_profile_dir_property(self, asset_manager, temp_assets_dir):
  192. """Profile-Dir Property funktioniert."""
  193. assert asset_manager.profile_dir == temp_assets_dir / "custom"
  194. def test_default_dir_property(self, asset_manager, temp_assets_dir):
  195. """Default-Dir Property funktioniert."""
  196. assert asset_manager.default_dir == temp_assets_dir / "default"
  197. class TestAssetManagerGet:
  198. """Tests für Asset-Abruf."""
  199. def test_get_from_profile(self, asset_manager, temp_assets_dir):
  200. """Asset wird aus Profil geladen."""
  201. path = asset_manager.get("audio/success.wav")
  202. assert path is not None
  203. assert path == temp_assets_dir / "custom" / "audio" / "success.wav"
  204. def test_get_fallback_to_default(self, asset_manager, temp_assets_dir):
  205. """Fallback auf Default funktioniert."""
  206. path = asset_manager.get("audio/error.wav")
  207. assert path is not None
  208. assert path == temp_assets_dir / "default" / "audio" / "error.wav"
  209. def test_get_not_found_returns_none(self, asset_manager):
  210. """Nicht gefundenes Asset gibt None zurück."""
  211. path = asset_manager.get("audio/nonexistent.wav")
  212. assert path is None
  213. def test_get_raises_on_missing(self, asset_manager):
  214. """AssetNotFoundError wird bei raise_on_missing geworfen."""
  215. with pytest.raises(AssetNotFoundError) as exc_info:
  216. asset_manager.get("audio/nonexistent.wav", raise_on_missing=True)
  217. assert "nonexistent.wav" in str(exc_info.value)
  218. assert exc_info.value.profile == "custom"
  219. def test_get_info_returns_details(self, asset_manager, temp_assets_dir):
  220. """get_info gibt detaillierte Informationen zurück."""
  221. info = asset_manager.get_info("audio/success.wav")
  222. assert info is not None
  223. assert info.profile == "custom"
  224. assert info.is_default is False
  225. assert info.asset_type == AssetType.AUDIO
  226. assert info.size == 154 # RIFF (4 bytes) + 150 bytes (custom profile)
  227. def test_get_info_default_fallback(self, asset_manager, temp_assets_dir):
  228. """get_info nutzt Default-Fallback."""
  229. info = asset_manager.get_info("audio/error.wav")
  230. assert info is not None
  231. assert info.profile == "default"
  232. assert info.is_default is True
  233. def test_get_info_not_found(self, asset_manager):
  234. """get_info gibt None zurück wenn nicht gefunden."""
  235. info = asset_manager.get_info("nonexistent.file")
  236. assert info is None
  237. def test_exists(self, asset_manager):
  238. """exists prüft korrekt."""
  239. assert asset_manager.exists("audio/success.wav") is True
  240. assert asset_manager.exists("audio/error.wav") is True
  241. assert asset_manager.exists("nonexistent.file") is False
  242. class TestAssetManagerTypedAccess:
  243. """Tests für typ-spezifische Zugriffe."""
  244. def test_get_audio(self, asset_manager, temp_assets_dir):
  245. """get_audio funktioniert."""
  246. path = asset_manager.get_audio("success.wav")
  247. assert path is not None
  248. assert "audio" in str(path)
  249. def test_get_music(self, asset_manager, temp_assets_dir):
  250. """get_music funktioniert."""
  251. path = asset_manager.get_music("song1.mp3")
  252. assert path is not None
  253. assert "music" in str(path)
  254. def test_get_image(self, asset_manager, temp_assets_dir):
  255. """get_image funktioniert."""
  256. path = asset_manager.get_image("logo.png")
  257. assert path is not None
  258. assert "images" in str(path)
  259. def test_get_nonexistent_typed(self, asset_manager):
  260. """Typ-Methoden geben None für nicht existente Assets."""
  261. assert asset_manager.get_audio("nonexistent.wav") is None
  262. assert asset_manager.get_music("nonexistent.mp3") is None
  263. assert asset_manager.get_image("nonexistent.png") is None
  264. class TestAssetManagerCaching:
  265. """Tests für das Cache-System."""
  266. def test_cache_hit(self, asset_manager):
  267. """Cache wird genutzt."""
  268. # Erster Aufruf
  269. info1 = asset_manager.get_info("audio/success.wav")
  270. # Zweiter Aufruf (sollte gecached sein)
  271. info2 = asset_manager.get_info("audio/success.wav")
  272. assert info1 is info2 # Gleiche Instanz
  273. def test_cache_disabled(self, temp_assets_dir):
  274. """Cache kann deaktiviert werden."""
  275. manager = AssetManager(
  276. base_dir=temp_assets_dir,
  277. profile="custom",
  278. cache_enabled=False,
  279. )
  280. info1 = manager.get_info("audio/success.wav")
  281. info2 = manager.get_info("audio/success.wav")
  282. # Nicht gleiche Instanz
  283. assert info1 is not info2
  284. def test_cache_clear(self, asset_manager):
  285. """Cache kann geleert werden."""
  286. # Cache füllen
  287. asset_manager.get_info("audio/success.wav")
  288. assert len(asset_manager._cache) > 0
  289. # Cache leeren
  290. asset_manager.clear_cache()
  291. assert len(asset_manager._cache) == 0
  292. def test_cache_ttl_expiration(self, temp_assets_dir):
  293. """Cache-Einträge verfallen nach TTL."""
  294. manager = AssetManager(
  295. base_dir=temp_assets_dir,
  296. profile="custom",
  297. cache_enabled=True,
  298. cache_ttl=0.1, # 100ms
  299. )
  300. # Cache füllen
  301. info1 = manager.get_info("audio/success.wav")
  302. assert "audio/success.wav" in manager._cache
  303. # Warten bis TTL abgelaufen
  304. time.sleep(0.15)
  305. # Neuer Abruf sollte Cache-Miss sein
  306. info2 = manager.get_info("audio/success.wav")
  307. # Neue Instanz
  308. assert info1 is not info2
  309. class TestAssetManagerListing:
  310. """Tests für Asset-Listing."""
  311. def test_list_assets_all(self, asset_manager):
  312. """Alle Assets werden gelistet."""
  313. assets = list(asset_manager.list_assets())
  314. # Sollte mindestens die bekannten Assets enthalten
  315. assert len(assets) >= 5 # success.wav, error.wav, custom_sound.wav, song1.mp3, logo.png, config.json
  316. def test_list_assets_by_type(self, asset_manager):
  317. """Filterung nach Typ funktioniert."""
  318. audio_assets = list(asset_manager.list_assets(AssetType.AUDIO))
  319. # Alle sollten Audio sein
  320. for info in audio_assets:
  321. assert info.asset_type == AssetType.AUDIO
  322. def test_list_assets_by_directory(self, asset_manager):
  323. """Filterung nach Verzeichnis funktioniert."""
  324. audio_assets = list(asset_manager.list_assets(directory="audio"))
  325. for info in audio_assets:
  326. assert "audio" in info.relative_path
  327. def test_list_assets_profile_priority(self, asset_manager, temp_assets_dir):
  328. """Profil-Assets haben Priorität über Default."""
  329. audio_assets = list(asset_manager.list_assets(directory="audio"))
  330. # Finde success.wav
  331. success_info = None
  332. for info in audio_assets:
  333. if info.name == "success.wav":
  334. success_info = info
  335. break
  336. assert success_info is not None
  337. assert success_info.profile == "custom"
  338. assert success_info.is_default is False
  339. def test_list_assets_exclude_default(self, asset_manager):
  340. """Default kann ausgeschlossen werden."""
  341. assets = list(asset_manager.list_assets(include_default=False))
  342. for info in assets:
  343. assert info.is_default is False
  344. def test_list_audio(self, asset_manager):
  345. """list_audio funktioniert."""
  346. audio_assets = list(asset_manager.list_audio())
  347. assert len(audio_assets) >= 3 # success, error, custom_sound
  348. def test_list_music(self, asset_manager):
  349. """list_music funktioniert."""
  350. music_assets = list(asset_manager.list_music())
  351. assert len(music_assets) >= 1 # song1.mp3
  352. def test_list_profiles(self, asset_manager):
  353. """list_profiles gibt alle Profile zurück."""
  354. profiles = asset_manager.list_profiles()
  355. assert "default" in profiles
  356. assert "custom" in profiles
  357. # Default sollte zuerst sein
  358. assert profiles[0] == "default"
  359. class TestAssetManagerProfileSwitch:
  360. """Tests für Profil-Wechsel."""
  361. def test_profile_switch_clears_cache(self, asset_manager):
  362. """Profil-Wechsel leert den Cache."""
  363. # Cache füllen
  364. asset_manager.get_info("audio/success.wav")
  365. assert len(asset_manager._cache) > 0
  366. # Profil wechseln
  367. asset_manager.profile = "default"
  368. # Cache sollte leer sein
  369. assert len(asset_manager._cache) == 0
  370. def test_profile_switch_creates_directory(self, temp_assets_dir):
  371. """Profil-Wechsel erstellt Verzeichnis."""
  372. manager = AssetManager(base_dir=temp_assets_dir, profile="default")
  373. # Neues Profil
  374. manager.profile = "new_profile"
  375. assert (temp_assets_dir / "new_profile").exists()
  376. def test_profile_switch_affects_resolution(self, asset_manager, temp_assets_dir):
  377. """Profil-Wechsel beeinflusst Pfad-Auflösung."""
  378. # Im custom-Profil
  379. info1 = asset_manager.get_info("audio/success.wav")
  380. assert info1.profile == "custom"
  381. # Wechsel zu default
  382. asset_manager.profile = "default"
  383. # Jetzt sollte default verwendet werden
  384. info2 = asset_manager.get_info("audio/success.wav")
  385. assert info2.profile == "default"
  386. class TestAssetManagerCopy:
  387. """Tests für Asset-Kopieren."""
  388. def test_copy_to_profile(self, asset_manager, temp_assets_dir):
  389. """Asset kann in Profil kopiert werden."""
  390. # error.wav ist nur in default
  391. assert asset_manager.get_info("audio/error.wav").is_default is True
  392. # Kopieren
  393. new_path = asset_manager.copy_to_profile("audio/error.wav")
  394. assert new_path is not None
  395. assert new_path.exists()
  396. assert "custom" in str(new_path)
  397. # Cache sollte invalidiert sein
  398. info = asset_manager.get_info("audio/error.wav")
  399. assert info.profile == "custom"
  400. assert info.is_default is False
  401. def test_copy_nonexistent_returns_none(self, asset_manager):
  402. """Kopieren nicht existenter Assets gibt None."""
  403. result = asset_manager.copy_to_profile("nonexistent.file")
  404. assert result is None
  405. def test_copy_already_in_profile(self, asset_manager, temp_assets_dir):
  406. """Kopieren eines bereits im Profil vorhandenen Assets."""
  407. # success.wav ist im custom-Profil
  408. result = asset_manager.copy_to_profile("audio/success.wav")
  409. # Sollte bestehenden Pfad zurückgeben
  410. assert result is not None
  411. assert "custom" in str(result)
  412. class TestAssetManagerStats:
  413. """Tests für Statistiken."""
  414. def test_get_stats(self, asset_manager):
  415. """get_stats gibt korrektes Dictionary zurück."""
  416. stats = asset_manager.get_stats()
  417. assert stats["profile"] == "custom"
  418. assert "default" in stats["profiles"]
  419. assert "custom" in stats["profiles"]
  420. assert stats["counts"]["total"] > 0
  421. assert stats["sizes"]["total"] > 0
  422. def test_stats_counts_types(self, asset_manager):
  423. """Stats zählen Asset-Typen."""
  424. stats = asset_manager.get_stats()
  425. assert stats["counts"]["audio"] >= 3
  426. assert stats["counts"]["music"] >= 1
  427. class TestAssetManagerTypeDetection:
  428. """Tests für Typ-Erkennung."""
  429. def test_detect_audio_type(self, asset_manager):
  430. """Audio-Dateien werden erkannt."""
  431. info = asset_manager.get_info("audio/success.wav")
  432. assert info.asset_type == AssetType.AUDIO
  433. def test_detect_music_type(self, asset_manager):
  434. """Musik-Dateien werden erkannt."""
  435. info = asset_manager.get_info("music/song1.mp3")
  436. # music/ Verzeichnis hat Vorrang
  437. assert info.asset_type == AssetType.MUSIC
  438. def test_detect_image_type(self, asset_manager):
  439. """Bild-Dateien werden erkannt."""
  440. info = asset_manager.get_info("images/logo.png")
  441. assert info.asset_type == AssetType.IMAGE
  442. def test_detect_config_type(self, asset_manager):
  443. """Konfig-Dateien werden erkannt."""
  444. info = asset_manager.get_info("config.json")
  445. assert info.asset_type == AssetType.CONFIG
  446. class TestAssetNotFoundError:
  447. """Tests für AssetNotFoundError."""
  448. def test_error_contains_details(self, asset_manager):
  449. """Error enthält hilfreiche Details."""
  450. try:
  451. asset_manager.get("nonexistent/file.wav", raise_on_missing=True)
  452. except AssetNotFoundError as e:
  453. assert e.asset_path == "nonexistent/file.wav"
  454. assert e.profile == "custom"
  455. assert len(e.searched_paths) == 2
  456. # =============================================================================
  457. # AssetService Tests
  458. # =============================================================================
  459. class TestAssetServiceInit:
  460. """Tests für AssetService-Initialisierung."""
  461. def test_init(self, mock_application):
  462. """Service kann initialisiert werden."""
  463. service = AssetService(mock_application)
  464. assert service._manager is None
  465. assert service.state.name == "STOPPED"
  466. def test_service_metadata(self, mock_application):
  467. """Service-Metadaten sind korrekt."""
  468. service = AssetService(mock_application)
  469. assert service.NAME == "AssetService"
  470. assert service.PRIORITY.name == "CRITICAL"
  471. assert service.GROUP.name == "UTILITY"
  472. @pytest.mark.asyncio
  473. class TestAssetServiceLifecycle:
  474. """Tests für Service-Lebenszyklus."""
  475. async def test_start(self, mock_application, tmp_path):
  476. """Service kann gestartet werden."""
  477. mock_application.config.assets_directory = str(tmp_path / "assets")
  478. mock_application.config.profile = "test_profile"
  479. service = AssetService(mock_application)
  480. await service.start()
  481. assert service.is_running
  482. assert service._manager is not None
  483. assert service.profile == "test_profile"
  484. async def test_stop(self, mock_application, tmp_path):
  485. """Service kann gestoppt werden."""
  486. mock_application.config.assets_directory = str(tmp_path / "assets")
  487. mock_application.config.profile = "default"
  488. service = AssetService(mock_application)
  489. await service.start()
  490. await service.stop()
  491. assert not service.is_running
  492. assert service._manager is None
  493. async def test_health_check(self, mock_application, tmp_path):
  494. """Health-Check funktioniert."""
  495. mock_application.config.assets_directory = str(tmp_path / "assets")
  496. mock_application.config.profile = "default"
  497. service = AssetService(mock_application)
  498. # Vor Start
  499. assert await service.health_check() is False
  500. await service.start()
  501. # Nach Start
  502. assert await service.health_check() is True
  503. await service.stop()
  504. async def test_manager_access_before_start(self, mock_application):
  505. """Manager-Zugriff vor Start wirft Fehler."""
  506. service = AssetService(mock_application)
  507. with pytest.raises(RuntimeError, match="nicht gestartet"):
  508. _ = service.manager
  509. @pytest.mark.asyncio
  510. class TestAssetServiceDelegation:
  511. """Tests für delegierte Methoden."""
  512. async def test_get_delegation(self, mock_application, temp_assets_dir):
  513. """get wird delegiert."""
  514. mock_application.config.assets_directory = str(temp_assets_dir)
  515. mock_application.config.profile = "custom"
  516. service = AssetService(mock_application)
  517. await service.start()
  518. path = service.get("audio/success.wav")
  519. assert path is not None
  520. await service.stop()
  521. async def test_exists_delegation(self, mock_application, temp_assets_dir):
  522. """exists wird delegiert."""
  523. mock_application.config.assets_directory = str(temp_assets_dir)
  524. mock_application.config.profile = "custom"
  525. service = AssetService(mock_application)
  526. await service.start()
  527. assert service.exists("audio/success.wav") is True
  528. assert service.exists("nonexistent.file") is False
  529. await service.stop()
  530. async def test_profile_property(self, mock_application, temp_assets_dir):
  531. """Profile-Property wird delegiert."""
  532. mock_application.config.assets_directory = str(temp_assets_dir)
  533. mock_application.config.profile = "custom"
  534. service = AssetService(mock_application)
  535. await service.start()
  536. assert service.profile == "custom"
  537. service.profile = "default"
  538. assert service.profile == "default"
  539. await service.stop()
  540. async def test_list_assets_delegation(self, mock_application, temp_assets_dir):
  541. """list_assets wird delegiert."""
  542. mock_application.config.assets_directory = str(temp_assets_dir)
  543. mock_application.config.profile = "custom"
  544. service = AssetService(mock_application)
  545. await service.start()
  546. assets = list(service.list_assets())
  547. assert len(assets) > 0
  548. await service.stop()
  549. async def test_get_stats_delegation(self, mock_application, temp_assets_dir):
  550. """get_stats wird delegiert."""
  551. mock_application.config.assets_directory = str(temp_assets_dir)
  552. mock_application.config.profile = "custom"
  553. service = AssetService(mock_application)
  554. await service.start()
  555. stats = service.get_stats()
  556. assert "profile" in stats
  557. assert "counts" in stats
  558. await service.stop()
  559. # =============================================================================
  560. # Integration mit echten Assets
  561. # =============================================================================
  562. class TestRealAssets:
  563. """Tests mit echten Assets aus dem Projekt."""
  564. @pytest.fixture
  565. def real_asset_manager(self):
  566. """AssetManager mit echten Projekt-Assets."""
  567. assets_path = Path(__file__).parent.parent / "assets"
  568. if not assets_path.exists():
  569. pytest.skip("Assets-Verzeichnis nicht gefunden")
  570. return AssetManager(base_dir=assets_path, profile="default")
  571. def test_real_audio_exists(self, real_asset_manager):
  572. """Echte Audio-Assets existieren."""
  573. path = real_asset_manager.get("audio/success.wav")
  574. if path is None:
  575. pytest.skip("success.wav nicht gefunden")
  576. assert path.exists()
  577. def test_real_music_listing(self, real_asset_manager):
  578. """Echte Musik-Assets können gelistet werden."""
  579. music = list(real_asset_manager.list_music())
  580. if len(music) == 0:
  581. pytest.skip("Keine Musik-Assets gefunden")
  582. for info in music:
  583. assert info.asset_type == AssetType.MUSIC
  584. assert info.exists
  585. def test_real_asset_info(self, real_asset_manager):
  586. """AssetInfo für echte Assets funktioniert."""
  587. info = real_asset_manager.get_info("audio/success.wav")
  588. if info is None:
  589. pytest.skip("success.wav nicht gefunden")
  590. assert info.size > 0
  591. assert info.modified > 0
  592. assert len(info.checksum) == 32
  593. # =============================================================================
  594. # Edge Cases
  595. # =============================================================================
  596. class TestEdgeCases:
  597. """Tests für Grenzfälle."""
  598. def test_path_normalization(self, asset_manager):
  599. """Pfade werden normalisiert."""
  600. # Backslashes
  601. path1 = asset_manager.get("audio\\success.wav")
  602. # Leading slash
  603. path2 = asset_manager.get("/audio/success.wav")
  604. assert path1 == path2
  605. def test_empty_profile(self, temp_assets_dir):
  606. """Leeres Profil verwendet Default."""
  607. manager = AssetManager(base_dir=temp_assets_dir, profile="")
  608. # Sollte trotzdem funktionieren
  609. assert manager.profile == ""
  610. assert manager.profile_dir == temp_assets_dir / ""
  611. def test_special_characters_in_filename(self, temp_assets_dir):
  612. """Sonderzeichen in Dateinamen werden unterstützt."""
  613. # Asset mit Sonderzeichen erstellen
  614. special_file = temp_assets_dir / "default" / "audio" / "test file (1).wav"
  615. special_file.write_bytes(b"RIFF" + b"\x00" * 50)
  616. manager = AssetManager(base_dir=temp_assets_dir, profile="default")
  617. path = manager.get("audio/test file (1).wav")
  618. assert path is not None
  619. assert path.exists()
  620. def test_unicode_in_filename(self, temp_assets_dir):
  621. """Unicode in Dateinamen wird unterstützt."""
  622. # Asset mit Unicode erstellen
  623. unicode_file = temp_assets_dir / "default" / "audio" / "müsik.wav"
  624. unicode_file.write_bytes(b"RIFF" + b"\x00" * 50)
  625. manager = AssetManager(base_dir=temp_assets_dir, profile="default")
  626. path = manager.get("audio/müsik.wav")
  627. assert path is not None
  628. assert path.exists()
  629. def test_nested_directories(self, temp_assets_dir):
  630. """Verschachtelte Verzeichnisse werden unterstützt."""
  631. # Verschachteltes Asset erstellen
  632. nested_dir = temp_assets_dir / "default" / "audio" / "effects" / "explosions"
  633. nested_dir.mkdir(parents=True)
  634. (nested_dir / "boom.wav").write_bytes(b"RIFF" + b"\x00" * 50)
  635. manager = AssetManager(base_dir=temp_assets_dir, profile="default")
  636. path = manager.get("audio/effects/explosions/boom.wav")
  637. assert path is not None
  638. assert path.exists()
  639. def test_concurrent_access(self, asset_manager):
  640. """Concurrent Zugriff ist thread-safe."""
  641. import concurrent.futures
  642. def access_asset(i):
  643. return asset_manager.get_info("audio/success.wav")
  644. with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
  645. futures = [executor.submit(access_asset, i) for i in range(50)]
  646. results = [f.result() for f in futures]
  647. # Alle sollten erfolgreich sein
  648. assert all(r is not None for r in results)
  649. if __name__ == "__main__":
  650. pytest.main([__file__, "-v"])