test_satellite_capability.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Satellite Capability System.
  4. """
  5. import asyncio
  6. import pytest
  7. from datetime import datetime
  8. from trixy_core.satellite.capability import (
  9. CapabilityDiscovery,
  10. DiscoveryConfig,
  11. CapabilityProbe,
  12. CapabilityRegistry,
  13. Capability,
  14. CapabilityInfo,
  15. CapabilityState,
  16. )
  17. from trixy_core.satellite.capability.discovery import SimpleProbe, ProbeResult
  18. class TestCapabilityInfo:
  19. """Tests für CapabilityInfo."""
  20. def test_info_creation(self):
  21. """Testet Info-Erstellung."""
  22. info = CapabilityInfo(
  23. id="audio.input",
  24. name="Mikrofon",
  25. description="Audio-Eingang",
  26. category="audio",
  27. )
  28. assert info.id == "audio.input"
  29. assert info.name == "Mikrofon"
  30. assert info.category == "audio"
  31. def test_info_defaults(self):
  32. """Testet Standard-Werte."""
  33. info = CapabilityInfo(id="test", name="Test")
  34. assert info.description == ""
  35. assert info.category == ""
  36. assert info.required_features == []
  37. class TestCapability:
  38. """Tests für Capability."""
  39. @pytest.fixture
  40. def cap_info(self):
  41. """Erstellt CapabilityInfo."""
  42. return CapabilityInfo(
  43. id="audio.input",
  44. name="Mikrofon",
  45. category="audio",
  46. )
  47. def test_capability_creation(self, cap_info):
  48. """Testet Capability-Erstellung."""
  49. cap = Capability(
  50. info=cap_info,
  51. state=CapabilityState.AVAILABLE,
  52. )
  53. assert cap.id == "audio.input"
  54. assert cap.state == CapabilityState.AVAILABLE
  55. assert cap.is_available
  56. def test_capability_states(self, cap_info):
  57. """Testet Capability-Zustände."""
  58. cap = Capability(info=cap_info)
  59. cap.state = CapabilityState.UNKNOWN
  60. assert not cap.is_available
  61. assert not cap.is_degraded
  62. cap.state = CapabilityState.AVAILABLE
  63. assert cap.is_available
  64. cap.state = CapabilityState.DEGRADED
  65. assert cap.is_degraded
  66. def test_update_state(self, cap_info):
  67. """Testet State-Update."""
  68. cap = Capability(info=cap_info)
  69. cap.update_state(CapabilityState.UNAVAILABLE, "Gerät nicht gefunden")
  70. assert cap.state == CapabilityState.UNAVAILABLE
  71. assert cap.error_message == "Gerät nicht gefunden"
  72. assert cap.last_checked is not None
  73. def test_to_dict(self, cap_info):
  74. """Testet Dictionary-Konvertierung."""
  75. cap = Capability(
  76. info=cap_info,
  77. state=CapabilityState.AVAILABLE,
  78. satellite_id="sat-001",
  79. )
  80. d = cap.to_dict()
  81. assert d["id"] == "audio.input"
  82. assert d["state"] == "AVAILABLE"
  83. assert d["satellite_id"] == "sat-001"
  84. class TestCapabilityRegistry:
  85. """Tests für CapabilityRegistry."""
  86. @pytest.fixture
  87. def registry(self):
  88. """Erstellt Registry."""
  89. return CapabilityRegistry()
  90. @pytest.fixture
  91. def audio_info(self):
  92. """Erstellt Audio-CapabilityInfo."""
  93. return CapabilityInfo(
  94. id="audio.input",
  95. name="Mikrofon",
  96. category="audio",
  97. )
  98. def test_define_capability(self, registry, audio_info):
  99. """Testet Capability-Definition."""
  100. registry.define_capability(audio_info)
  101. assert registry.capability_count == 1
  102. assert registry.get_definition("audio.input") == audio_info
  103. def test_register_capability(self, registry, audio_info):
  104. """Testet Capability-Registrierung."""
  105. cap = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  106. registry.register("sat-001", cap)
  107. assert registry.satellite_count == 1
  108. assert registry.has_capability("sat-001", "audio.input")
  109. def test_unregister_capability(self, registry, audio_info):
  110. """Testet Capability-Entfernung."""
  111. cap = Capability(info=audio_info)
  112. registry.register("sat-001", cap)
  113. assert registry.unregister("sat-001", "audio.input")
  114. assert not registry.has_capability("sat-001", "audio.input")
  115. def test_unregister_all_satellite(self, registry, audio_info):
  116. """Testet Entfernung aller Capabilities eines Satellites."""
  117. cap = Capability(info=audio_info)
  118. registry.register("sat-001", cap)
  119. assert registry.unregister("sat-001")
  120. assert "sat-001" not in registry
  121. def test_update_state(self, registry, audio_info):
  122. """Testet State-Update über Registry."""
  123. cap = Capability(info=audio_info)
  124. registry.register("sat-001", cap)
  125. assert registry.update_state(
  126. "sat-001",
  127. "audio.input",
  128. CapabilityState.UNAVAILABLE,
  129. "Fehler",
  130. )
  131. updated = registry.get("sat-001", "audio.input")
  132. assert updated.state == CapabilityState.UNAVAILABLE
  133. def test_get_capability(self, registry, audio_info):
  134. """Testet Capability-Abruf."""
  135. cap = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  136. registry.register("sat-001", cap)
  137. result = registry.get("sat-001", "audio.input")
  138. assert result == cap
  139. def test_get_all_capabilities(self, registry, audio_info):
  140. """Testet Abruf aller Capabilities."""
  141. cap1 = Capability(
  142. info=audio_info,
  143. state=CapabilityState.AVAILABLE,
  144. )
  145. cap2 = Capability(
  146. info=CapabilityInfo("display.screen", "Bildschirm"),
  147. state=CapabilityState.AVAILABLE,
  148. )
  149. registry.register("sat-001", cap1)
  150. registry.register("sat-001", cap2)
  151. caps = registry.get_all("sat-001")
  152. assert len(caps) == 2
  153. def test_get_available_capabilities(self, registry, audio_info):
  154. """Testet Abruf verfügbarer Capabilities."""
  155. cap1 = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  156. cap2 = Capability(
  157. info=CapabilityInfo("display.screen", "Bildschirm"),
  158. state=CapabilityState.UNAVAILABLE,
  159. )
  160. registry.register("sat-001", cap1)
  161. registry.register("sat-001", cap2)
  162. available = registry.get_available("sat-001")
  163. assert len(available) == 1
  164. assert available[0].id == "audio.input"
  165. def test_has_capability(self, registry, audio_info):
  166. """Testet Capability-Prüfung."""
  167. cap = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  168. registry.register("sat-001", cap)
  169. assert registry.has_capability("sat-001", "audio.input")
  170. assert not registry.has_capability("sat-001", "unknown")
  171. assert not registry.has_capability("unknown", "audio.input")
  172. def test_has_capability_require_available(self, registry, audio_info):
  173. """Testet Capability-Prüfung mit Verfügbarkeit."""
  174. cap = Capability(info=audio_info, state=CapabilityState.UNAVAILABLE)
  175. registry.register("sat-001", cap)
  176. # Mit require_available=True
  177. assert not registry.has_capability("sat-001", "audio.input", True)
  178. # Mit require_available=False
  179. assert registry.has_capability("sat-001", "audio.input", False)
  180. def test_has_all_capabilities(self, registry):
  181. """Testet Prüfung mehrerer Capabilities."""
  182. cap1 = Capability(
  183. info=CapabilityInfo("cap1", "Cap 1"),
  184. state=CapabilityState.AVAILABLE,
  185. )
  186. cap2 = Capability(
  187. info=CapabilityInfo("cap2", "Cap 2"),
  188. state=CapabilityState.AVAILABLE,
  189. )
  190. registry.register("sat-001", cap1)
  191. registry.register("sat-001", cap2)
  192. assert registry.has_all_capabilities("sat-001", ["cap1", "cap2"])
  193. assert not registry.has_all_capabilities("sat-001", ["cap1", "cap3"])
  194. def test_has_any_capability(self, registry):
  195. """Testet Prüfung auf mindestens eine Capability."""
  196. cap = Capability(
  197. info=CapabilityInfo("cap1", "Cap 1"),
  198. state=CapabilityState.AVAILABLE,
  199. )
  200. registry.register("sat-001", cap)
  201. assert registry.has_any_capability("sat-001", ["cap1", "cap2"])
  202. assert not registry.has_any_capability("sat-001", ["cap3", "cap4"])
  203. def test_find_satellites_with(self, registry, audio_info):
  204. """Testet Satellite-Suche nach Capability."""
  205. cap = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  206. registry.register("sat-001", cap)
  207. registry.register("sat-002", cap)
  208. satellites = registry.find_satellites_with("audio.input")
  209. assert len(satellites) == 2
  210. assert "sat-001" in satellites
  211. assert "sat-002" in satellites
  212. def test_find_satellites_with_all(self, registry):
  213. """Testet Satellite-Suche nach mehreren Capabilities."""
  214. cap1 = Capability(
  215. info=CapabilityInfo("cap1", "Cap 1"),
  216. state=CapabilityState.AVAILABLE,
  217. )
  218. cap2 = Capability(
  219. info=CapabilityInfo("cap2", "Cap 2"),
  220. state=CapabilityState.AVAILABLE,
  221. )
  222. registry.register("sat-001", cap1)
  223. registry.register("sat-001", cap2)
  224. registry.register("sat-002", cap1) # Nur cap1
  225. satellites = registry.find_satellites_with_all(["cap1", "cap2"])
  226. assert satellites == ["sat-001"]
  227. def test_get_by_category(self, registry):
  228. """Testet Abruf nach Kategorie."""
  229. cap1 = Capability(
  230. info=CapabilityInfo("audio.in", "In", category="audio"),
  231. )
  232. cap2 = Capability(
  233. info=CapabilityInfo("audio.out", "Out", category="audio"),
  234. )
  235. cap3 = Capability(
  236. info=CapabilityInfo("display", "Display", category="display"),
  237. )
  238. registry.register("sat-001", cap1)
  239. registry.register("sat-001", cap2)
  240. registry.register("sat-001", cap3)
  241. audio_caps = registry.get_by_category("sat-001", "audio")
  242. assert len(audio_caps) == 2
  243. def test_on_change_callback(self, registry, audio_info):
  244. """Testet Änderungs-Callback."""
  245. changes = []
  246. def on_change(sat_id, cap):
  247. changes.append((sat_id, cap.id))
  248. registry.on_change(on_change)
  249. cap = Capability(info=audio_info)
  250. registry.register("sat-001", cap)
  251. assert ("sat-001", "audio.input") in changes
  252. def test_get_summary(self, registry, audio_info):
  253. """Testet Zusammenfassung."""
  254. cap = Capability(info=audio_info, state=CapabilityState.AVAILABLE)
  255. registry.register("sat-001", cap)
  256. summary = registry.get_summary()
  257. assert summary["satellite_count"] == 1
  258. assert summary["total_capabilities"] == 1
  259. assert summary["available_capabilities"] == 1
  260. class TestDiscoveryConfig:
  261. """Tests für DiscoveryConfig."""
  262. def test_default_config(self):
  263. """Testet Standard-Konfiguration."""
  264. config = DiscoveryConfig()
  265. assert config.probe_timeout == 10.0
  266. assert config.probe_concurrency == 5
  267. assert config.auto_discover is True
  268. class TestProbeResult:
  269. """Tests für ProbeResult."""
  270. def test_result_creation(self):
  271. """Testet Result-Erstellung."""
  272. result = ProbeResult(
  273. capability_id="audio.input",
  274. success=True,
  275. state=CapabilityState.AVAILABLE,
  276. )
  277. assert result.capability_id == "audio.input"
  278. assert result.success
  279. assert result.state == CapabilityState.AVAILABLE
  280. class TestCapabilityDiscovery:
  281. """Tests für CapabilityDiscovery."""
  282. @pytest.fixture
  283. def registry(self):
  284. """Erstellt Registry."""
  285. return CapabilityRegistry()
  286. @pytest.fixture
  287. def send_func(self):
  288. """Erstellt Mock-Send-Funktion."""
  289. responses = {}
  290. async def send(sat_id, message):
  291. return responses.get(sat_id, {"available": False})
  292. send.responses = responses
  293. return send
  294. @pytest.fixture
  295. def discovery(self, registry, send_func):
  296. """Erstellt Discovery-Instanz."""
  297. config = DiscoveryConfig(
  298. probe_timeout=1.0,
  299. probe_concurrency=2,
  300. )
  301. return CapabilityDiscovery(
  302. registry=registry,
  303. send_func=send_func,
  304. config=config,
  305. )
  306. def test_register_probe(self, discovery):
  307. """Testet Probe-Registrierung."""
  308. info = CapabilityInfo("test", "Test")
  309. probe = SimpleProbe(info)
  310. discovery.register_probe(probe)
  311. assert discovery.probe_count == 1
  312. assert discovery.registry.get_definition("test") is not None
  313. def test_unregister_probe(self, discovery):
  314. """Testet Probe-Entfernung."""
  315. info = CapabilityInfo("test", "Test")
  316. probe = SimpleProbe(info)
  317. discovery.register_probe(probe)
  318. assert discovery.unregister_probe("test")
  319. assert discovery.probe_count == 0
  320. @pytest.mark.asyncio
  321. async def test_discover_capability(self, discovery, send_func):
  322. """Testet einzelne Capability-Discovery."""
  323. info = CapabilityInfo("audio.input", "Mikrofon")
  324. probe = SimpleProbe(info)
  325. discovery.register_probe(probe)
  326. send_func.responses["sat-001"] = {
  327. "available": True,
  328. "parameters": {"channels": 2},
  329. }
  330. result = await discovery.discover_capability("sat-001", "audio.input")
  331. assert result is not None
  332. assert result.success
  333. assert result.state == CapabilityState.AVAILABLE
  334. @pytest.mark.asyncio
  335. async def test_discover_satellite(self, discovery, send_func):
  336. """Testet vollständige Satellite-Discovery."""
  337. info1 = CapabilityInfo("cap1", "Cap 1")
  338. info2 = CapabilityInfo("cap2", "Cap 2")
  339. discovery.register_probe(SimpleProbe(info1))
  340. discovery.register_probe(SimpleProbe(info2))
  341. send_func.responses["sat-001"] = {"available": True}
  342. results = await discovery.discover_satellite("sat-001")
  343. assert len(results) == 2
  344. assert all(r.success for r in results)
  345. @pytest.mark.asyncio
  346. async def test_discover_specific_capabilities(self, discovery, send_func):
  347. """Testet Discovery spezifischer Capabilities."""
  348. info1 = CapabilityInfo("cap1", "Cap 1")
  349. info2 = CapabilityInfo("cap2", "Cap 2")
  350. discovery.register_probe(SimpleProbe(info1))
  351. discovery.register_probe(SimpleProbe(info2))
  352. send_func.responses["sat-001"] = {"available": True}
  353. results = await discovery.discover_satellite("sat-001", ["cap1"])
  354. assert len(results) == 1
  355. assert results[0].capability_id == "cap1"
  356. @pytest.mark.asyncio
  357. async def test_discover_all(self, discovery, send_func):
  358. """Testet Discovery für mehrere Satellites."""
  359. info = CapabilityInfo("cap1", "Cap 1")
  360. discovery.register_probe(SimpleProbe(info))
  361. send_func.responses["sat-001"] = {"available": True}
  362. send_func.responses["sat-002"] = {"available": False}
  363. results = await discovery.discover_all(["sat-001", "sat-002"])
  364. assert "sat-001" in results
  365. assert "sat-002" in results
  366. def test_needs_rediscovery(self, discovery):
  367. """Testet Rediscovery-Prüfung."""
  368. assert discovery.needs_rediscovery("sat-001")
  369. discovery._last_discovery["sat-001"] = datetime.now()
  370. assert not discovery.needs_rediscovery("sat-001")
  371. def test_get_stats(self, discovery):
  372. """Testet Statistik-Abruf."""
  373. info = CapabilityInfo("test", "Test")
  374. discovery.register_probe(SimpleProbe(info))
  375. stats = discovery.get_stats()
  376. assert stats["probe_count"] == 1
  377. assert "config" in stats