test_plugin_extensions.py 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Plugin Extensions (Phase 6).
  4. Testet Versionierung, Abhängigkeiten, Registry, Hooks,
  5. Hot-Reload und Testing-Utilities.
  6. """
  7. import asyncio
  8. import json
  9. import tempfile
  10. from pathlib import Path
  11. import pytest
  12. # ============================================================================
  13. # Versioning Tests
  14. # ============================================================================
  15. class TestSemanticVersion:
  16. """Tests für SemanticVersion."""
  17. def test_parse_basic(self):
  18. """Testet grundlegendes Parsing."""
  19. from trixy_core.plugins.versioning import parse_version
  20. v = parse_version("1.2.3")
  21. assert v.major == 1
  22. assert v.minor == 2
  23. assert v.patch == 3
  24. assert v.prerelease is None
  25. assert v.build is None
  26. def test_parse_with_prerelease(self):
  27. """Testet Parsing mit Prerelease."""
  28. from trixy_core.plugins.versioning import parse_version
  29. v = parse_version("1.0.0-alpha.1")
  30. assert v.major == 1
  31. assert v.prerelease == "alpha.1"
  32. def test_parse_with_build(self):
  33. """Testet Parsing mit Build-Metadaten."""
  34. from trixy_core.plugins.versioning import parse_version
  35. v = parse_version("1.0.0+build.123")
  36. assert v.build == "build.123"
  37. def test_parse_full(self):
  38. """Testet vollständiges Parsing."""
  39. from trixy_core.plugins.versioning import parse_version
  40. v = parse_version("2.0.0-beta.1+build.456")
  41. assert v.major == 2
  42. assert v.minor == 0
  43. assert v.patch == 0
  44. assert v.prerelease == "beta.1"
  45. assert v.build == "build.456"
  46. def test_comparison(self):
  47. """Testet Versionsvergleiche."""
  48. from trixy_core.plugins.versioning import parse_version
  49. v1 = parse_version("1.0.0")
  50. v2 = parse_version("2.0.0")
  51. v3 = parse_version("1.1.0")
  52. v4 = parse_version("1.0.1")
  53. assert v1 < v2
  54. assert v1 < v3
  55. assert v1 < v4
  56. assert v3 < v2
  57. assert v4 < v3
  58. def test_prerelease_comparison(self):
  59. """Testet Prerelease-Vergleiche."""
  60. from trixy_core.plugins.versioning import parse_version
  61. v1 = parse_version("1.0.0-alpha")
  62. v2 = parse_version("1.0.0-beta")
  63. v3 = parse_version("1.0.0")
  64. # Prerelease < Release
  65. assert v1 < v3
  66. assert v2 < v3
  67. # Alpha < Beta
  68. assert v1 < v2
  69. def test_bump_versions(self):
  70. """Testet Version-Bumping."""
  71. from trixy_core.plugins.versioning import parse_version
  72. v = parse_version("1.2.3")
  73. major = v.bump_major()
  74. assert str(major) == "2.0.0"
  75. minor = v.bump_minor()
  76. assert str(minor) == "1.3.0"
  77. patch = v.bump_patch()
  78. assert str(patch) == "1.2.4"
  79. def test_string_representation(self):
  80. """Testet String-Darstellung."""
  81. from trixy_core.plugins.versioning import parse_version
  82. v = parse_version("1.2.3-alpha+build")
  83. assert str(v) == "1.2.3-alpha+build"
  84. class TestVersionRange:
  85. """Tests für VersionRange."""
  86. def test_caret_range(self):
  87. """Testet Caret-Ranges (^)."""
  88. from trixy_core.plugins.versioning import parse_range, parse_version
  89. range_ = parse_range("^1.2.3")
  90. assert range_.satisfies(parse_version("1.2.3"))
  91. assert range_.satisfies(parse_version("1.2.4"))
  92. assert range_.satisfies(parse_version("1.3.0"))
  93. assert range_.satisfies(parse_version("1.9.9"))
  94. assert not range_.satisfies(parse_version("2.0.0"))
  95. assert not range_.satisfies(parse_version("1.2.2"))
  96. def test_tilde_range(self):
  97. """Testet Tilde-Ranges (~)."""
  98. from trixy_core.plugins.versioning import parse_range, parse_version
  99. range_ = parse_range("~1.2.3")
  100. assert range_.satisfies(parse_version("1.2.3"))
  101. assert range_.satisfies(parse_version("1.2.4"))
  102. assert range_.satisfies(parse_version("1.2.99"))
  103. assert not range_.satisfies(parse_version("1.3.0"))
  104. assert not range_.satisfies(parse_version("2.0.0"))
  105. def test_exact_range(self):
  106. """Testet exakte Versionen."""
  107. from trixy_core.plugins.versioning import parse_range, parse_version
  108. range_ = parse_range("1.2.3")
  109. assert range_.satisfies(parse_version("1.2.3"))
  110. assert not range_.satisfies(parse_version("1.2.4"))
  111. def test_comparison_ranges(self):
  112. """Testet Vergleichs-Ranges."""
  113. from trixy_core.plugins.versioning import parse_range, parse_version
  114. # >= 1.0.0
  115. range1 = parse_range(">=1.0.0")
  116. assert range1.satisfies(parse_version("1.0.0"))
  117. assert range1.satisfies(parse_version("2.0.0"))
  118. assert not range1.satisfies(parse_version("0.9.0"))
  119. # < 2.0.0
  120. range2 = parse_range("<2.0.0")
  121. assert range2.satisfies(parse_version("1.9.9"))
  122. assert not range2.satisfies(parse_version("2.0.0"))
  123. def test_wildcard_range(self):
  124. """Testet Wildcard-Ranges."""
  125. from trixy_core.plugins.versioning import parse_range, parse_version
  126. range_ = parse_range("1.2.*")
  127. assert range_.satisfies(parse_version("1.2.0"))
  128. assert range_.satisfies(parse_version("1.2.99"))
  129. assert not range_.satisfies(parse_version("1.3.0"))
  130. class TestVersionChecker:
  131. """Tests für VersionChecker."""
  132. def test_check_compatibility_exact(self):
  133. """Testet exakte Kompatibilität."""
  134. from trixy_core.plugins.versioning import VersionChecker, CompatibilityLevel
  135. checker = VersionChecker()
  136. result = checker.check("1.0.0", "1.0.0")
  137. assert result.compatible
  138. assert result.level == CompatibilityLevel.EXACT
  139. def test_check_compatibility_patch(self):
  140. """Testet Patch-Kompatibilität."""
  141. from trixy_core.plugins.versioning import VersionChecker, CompatibilityLevel
  142. checker = VersionChecker()
  143. # Mit Range ^1.0.0 sind Patch-Updates kompatibel
  144. result = checker.check("^1.0.0", "1.0.1")
  145. assert result.compatible
  146. assert result.level == CompatibilityLevel.COMPATIBLE
  147. def test_check_dependency(self):
  148. """Testet Abhängigkeitsprüfung."""
  149. from trixy_core.plugins.versioning import VersionChecker
  150. checker = VersionChecker()
  151. # Version erfüllt Range
  152. result = checker.check("^1.0.0", "1.5.0")
  153. assert result.compatible
  154. # Version erfüllt Range nicht
  155. result = checker.check("^2.0.0", "1.5.0")
  156. assert not result.compatible
  157. def test_check_dependencies(self):
  158. """Testet mehrere Abhängigkeiten."""
  159. from trixy_core.plugins.versioning import VersionChecker
  160. checker = VersionChecker()
  161. required = {"core": "^1.0.0", "utils": "~2.0.0"}
  162. available = {"core": "1.5.0", "utils": "2.0.5"}
  163. results = checker.check_dependencies(required, available)
  164. assert results["core"].compatible
  165. assert results["utils"].compatible
  166. # ============================================================================
  167. # Dependencies Tests
  168. # ============================================================================
  169. class TestDependencyGraph:
  170. """Tests für DependencyGraph."""
  171. def test_add_node(self):
  172. """Testet Hinzufügen von Knoten."""
  173. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode
  174. graph = DependencyGraph()
  175. node = DependencyNode(name="plugin_a", version="1.0.0")
  176. graph.add_node(node)
  177. assert "plugin_a" in graph
  178. assert graph.node_count == 1
  179. def test_add_edge_via_dependencies(self):
  180. """Testet Hinzufügen von Kanten via Abhängigkeiten."""
  181. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode
  182. graph = DependencyGraph()
  183. graph.add_node(DependencyNode(name="plugin_b", version="1.0.0"))
  184. graph.add_node(DependencyNode(
  185. name="plugin_a",
  186. version="1.0.0",
  187. dependencies={"plugin_b": "^1.0.0"},
  188. ))
  189. deps = graph.get_dependencies("plugin_a")
  190. assert "plugin_b" in deps
  191. def test_topological_sort(self):
  192. """Testet topologische Sortierung."""
  193. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode
  194. graph = DependencyGraph()
  195. # Einfacher Test: Zwei Plugins ohne Abhängigkeiten
  196. graph.add_node(DependencyNode(name="plugin_a", version="1.0.0"))
  197. graph.add_node(DependencyNode(name="plugin_b", version="1.0.0"))
  198. order = graph.get_load_order()
  199. # Beide sollten in der Ladereihenfolge sein
  200. assert "plugin_a" in order
  201. assert "plugin_b" in order
  202. assert len(order) == 2
  203. def test_cycle_detection(self):
  204. """Testet Zykluserkennung."""
  205. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode, CycleError
  206. graph = DependencyGraph()
  207. graph.add_node(DependencyNode(
  208. name="plugin_a",
  209. version="1.0.0",
  210. dependencies={"plugin_b": "^1.0.0"},
  211. ))
  212. graph.add_node(DependencyNode(
  213. name="plugin_b",
  214. version="1.0.0",
  215. dependencies={"plugin_a": "^1.0.0"},
  216. ))
  217. with pytest.raises(CycleError):
  218. graph.get_load_order()
  219. def test_has_cycle(self):
  220. """Testet Zyklus-Check ohne Exception."""
  221. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode
  222. graph = DependencyGraph()
  223. graph.add_node(DependencyNode(name="plugin_a", version="1.0.0"))
  224. graph.add_node(DependencyNode(
  225. name="plugin_b",
  226. version="1.0.0",
  227. dependencies={"plugin_a": "^1.0.0"},
  228. ))
  229. assert not graph.has_cycle()
  230. # Graph mit Zyklus
  231. graph2 = DependencyGraph()
  232. graph2.add_node(DependencyNode(
  233. name="plugin_a",
  234. version="1.0.0",
  235. dependencies={"plugin_b": "^1.0.0"},
  236. ))
  237. graph2.add_node(DependencyNode(
  238. name="plugin_b",
  239. version="1.0.0",
  240. dependencies={"plugin_a": "^1.0.0"},
  241. ))
  242. assert graph2.has_cycle()
  243. def test_export_dot(self):
  244. """Testet DOT-Export."""
  245. from trixy_core.plugins.dependencies import DependencyGraph, DependencyNode
  246. graph = DependencyGraph()
  247. graph.add_node(DependencyNode(name="plugin_b", version="1.0.0"))
  248. graph.add_node(DependencyNode(
  249. name="plugin_a",
  250. version="1.0.0",
  251. dependencies={"plugin_b": "^1.0.0"},
  252. ))
  253. dot = graph.to_dot()
  254. assert "digraph" in dot
  255. assert "plugin_a" in dot
  256. assert "plugin_b" in dot
  257. class TestDependencyResolver:
  258. """Tests für DependencyResolver."""
  259. def test_resolve_simple(self):
  260. """Testet einfache Auflösung."""
  261. from trixy_core.plugins.dependencies import DependencyResolver
  262. resolver = DependencyResolver()
  263. # Unabhängige Plugins registrieren
  264. resolver.register_plugin("plugin_a", "1.0.0", {})
  265. resolver.register_plugin("plugin_b", "1.0.0", {})
  266. # Alle Plugins auflösen (ohne Abhängigkeiten)
  267. result = resolver.resolve()
  268. assert result.success
  269. assert len(result.load_order) == 2
  270. def test_resolve_missing_dependency(self):
  271. """Testet fehlende Abhängigkeit."""
  272. from trixy_core.plugins.dependencies import DependencyResolver
  273. resolver = DependencyResolver()
  274. resolver.register_plugin("plugin_a", "1.0.0", {"plugin_missing": "^1.0.0"})
  275. result = resolver.resolve(["plugin_a"])
  276. assert not result.success
  277. assert len(result.missing) > 0
  278. # ============================================================================
  279. # Registry Tests
  280. # ============================================================================
  281. class TestPluginManifest:
  282. """Tests für PluginManifest."""
  283. def test_create_manifest(self):
  284. """Testet Manifest-Erstellung."""
  285. from trixy_core.plugins.registry import PluginManifest, PluginCapability
  286. manifest = PluginManifest(
  287. name="test_plugin",
  288. version="1.0.0",
  289. description="Test-Plugin",
  290. author="Test",
  291. capabilities=[PluginCapability.AUDIO_INPUT],
  292. )
  293. assert manifest.name == "test_plugin"
  294. assert manifest.version == "1.0.0"
  295. assert PluginCapability.AUDIO_INPUT in manifest.capabilities
  296. def test_manifest_to_dict(self):
  297. """Testet Dictionary-Konvertierung."""
  298. from trixy_core.plugins.registry import PluginManifest
  299. manifest = PluginManifest(
  300. name="test_plugin",
  301. version="1.0.0",
  302. )
  303. data = manifest.to_dict()
  304. assert data["name"] == "test_plugin"
  305. assert data["version"] == "1.0.0"
  306. def test_manifest_from_dict(self):
  307. """Testet Erstellung aus Dictionary."""
  308. from trixy_core.plugins.registry import PluginManifest
  309. data = {
  310. "name": "my_plugin",
  311. "version": "2.0.0",
  312. "description": "Mein Plugin",
  313. "author": "Autor",
  314. }
  315. manifest = PluginManifest.from_dict(data)
  316. assert manifest.name == "my_plugin"
  317. assert manifest.version == "2.0.0"
  318. def test_load_save_manifest(self):
  319. """Testet Laden und Speichern."""
  320. from trixy_core.plugins.registry import (
  321. PluginManifest,
  322. load_manifest,
  323. save_manifest,
  324. )
  325. with tempfile.TemporaryDirectory() as tmpdir:
  326. manifest = PluginManifest(
  327. name="test_plugin",
  328. version="1.0.0",
  329. )
  330. path = Path(tmpdir) / "config.json"
  331. save_manifest(manifest, path)
  332. loaded = load_manifest(path)
  333. assert loaded.name == manifest.name
  334. assert loaded.version == manifest.version
  335. class TestPluginDiscovery:
  336. """Tests für PluginDiscovery."""
  337. def test_discover_plugins(self):
  338. """Testet Plugin-Entdeckung."""
  339. from trixy_core.plugins.registry import PluginDiscovery
  340. from trixy_core.plugins.testing import PluginTestFixture
  341. with PluginTestFixture() as fixture:
  342. fixture.create_plugin("plugin_a")
  343. fixture.create_plugin("plugin_b")
  344. discovery = PluginDiscovery()
  345. results = discovery.scan(fixture.base_dir)
  346. assert len(results) == 2
  347. names = [r.name for r in results]
  348. assert "plugin_a" in names
  349. assert "plugin_b" in names
  350. class TestPluginRegistry:
  351. """Tests für PluginRegistry."""
  352. def test_register_plugin(self):
  353. """Testet Plugin-Registrierung."""
  354. from trixy_core.plugins.registry import PluginRegistry, PluginManifest
  355. registry = PluginRegistry()
  356. manifest = PluginManifest(
  357. name="test_plugin",
  358. version="1.0.0",
  359. )
  360. registry.register(manifest)
  361. assert "test_plugin" in registry
  362. entry = registry.get("test_plugin")
  363. assert entry is not None
  364. assert entry.manifest.name == "test_plugin"
  365. def test_list_plugins(self):
  366. """Testet Plugin-Auflistung."""
  367. from trixy_core.plugins.registry import PluginRegistry, PluginManifest
  368. registry = PluginRegistry()
  369. for i in range(3):
  370. manifest = PluginManifest(
  371. name=f"plugin_{i}",
  372. version="1.0.0",
  373. )
  374. registry.register(manifest)
  375. plugins = registry.get_all()
  376. assert len(plugins) == 3
  377. # ============================================================================
  378. # Hooks Tests
  379. # ============================================================================
  380. class TestPluginHook:
  381. """Tests für PluginHook."""
  382. def test_register_handler(self):
  383. """Testet Handler-Registrierung."""
  384. from trixy_core.plugins.hooks import PluginHook
  385. hook = PluginHook[str]("test_hook")
  386. @hook.register("test_plugin")
  387. def handler(value: str) -> str:
  388. return value.upper()
  389. assert hook.handler_count == 1
  390. def test_apply_hook(self):
  391. """Testet Hook-Anwendung."""
  392. from trixy_core.plugins.hooks import PluginHook
  393. hook = PluginHook[str]("test_hook")
  394. @hook.register("test_plugin")
  395. def handler(value: str) -> str:
  396. return value.upper()
  397. result = hook.apply("hello")
  398. assert result.value == "HELLO"
  399. assert result.modified
  400. def test_apply_multiple_handlers(self):
  401. """Testet mehrere Handler."""
  402. from trixy_core.plugins.hooks import PluginHook, HookPriority
  403. hook = PluginHook[str]("test_hook")
  404. @hook.register("plugin_a", priority=HookPriority.FIRST)
  405. def first_handler(value: str) -> str:
  406. return value + "_a"
  407. @hook.register("plugin_b", priority=HookPriority.LAST)
  408. def last_handler(value: str) -> str:
  409. return value + "_b"
  410. result = hook.apply("test")
  411. # Erst A, dann B
  412. assert result.value == "test_a_b"
  413. def test_apply_async(self):
  414. """Testet asynchrone Anwendung."""
  415. from trixy_core.plugins.hooks import PluginHook
  416. hook = PluginHook[str]("test_hook")
  417. @hook.register_async("test_plugin")
  418. async def async_handler(value: str) -> str:
  419. await asyncio.sleep(0.01)
  420. return value.upper()
  421. async def run_test():
  422. result = await hook.apply_async("hello")
  423. assert result.value == "HELLO"
  424. asyncio.run(run_test())
  425. def test_once_handler(self):
  426. """Testet Once-Handler."""
  427. from trixy_core.plugins.hooks import PluginHook
  428. hook = PluginHook[int]("test_hook")
  429. @hook.register("test_plugin", once=True)
  430. def once_handler(value: int) -> int:
  431. return value + 1
  432. # Erster Aufruf
  433. result1 = hook.apply(0)
  434. assert result1.value == 1
  435. assert hook.handler_count == 0 # Handler entfernt
  436. # Zweiter Aufruf
  437. result2 = hook.apply(0)
  438. assert result2.value == 0 # Nicht mehr modifiziert
  439. def test_trigger(self):
  440. """Testet Hook als Action."""
  441. from trixy_core.plugins.hooks import PluginHook
  442. hook = PluginHook("test_hook")
  443. called = []
  444. @hook.register("test_plugin")
  445. def handler(data):
  446. called.append(data)
  447. hook.trigger("test_data")
  448. assert "test_data" in called
  449. class TestHookRegistry:
  450. """Tests für HookRegistry."""
  451. def test_create_hook(self):
  452. """Testet Hook-Erstellung."""
  453. from trixy_core.plugins.hooks import HookRegistry
  454. registry = HookRegistry()
  455. hook = registry.create("test_hook", "Test-Beschreibung")
  456. assert registry.has_hook("test_hook")
  457. assert hook.name == "test_hook"
  458. def test_get_or_create(self):
  459. """Testet get_or_create."""
  460. from trixy_core.plugins.hooks import HookRegistry
  461. registry = HookRegistry()
  462. hook1 = registry.get_or_create("test_hook")
  463. hook2 = registry.get_or_create("test_hook")
  464. assert hook1 is hook2
  465. def test_register_handler(self):
  466. """Testet Handler-Registrierung."""
  467. from trixy_core.plugins.hooks import HookRegistry
  468. registry = HookRegistry()
  469. registry.register_handler(
  470. "test_hook",
  471. "test_plugin",
  472. lambda x: x.upper(),
  473. )
  474. result = registry.apply("test_hook", "hello")
  475. assert result.value == "HELLO"
  476. def test_remove_plugin(self):
  477. """Testet Plugin-Entfernung."""
  478. from trixy_core.plugins.hooks import HookRegistry
  479. registry = HookRegistry()
  480. registry.register_handler("hook_a", "test_plugin", lambda x: x)
  481. registry.register_handler("hook_b", "test_plugin", lambda x: x)
  482. removed = registry.remove_plugin("test_plugin")
  483. assert removed == 2
  484. # ============================================================================
  485. # Hot-Reload Tests
  486. # ============================================================================
  487. class TestFileWatcher:
  488. """Tests für FileWatcher."""
  489. def test_add_directory(self):
  490. """Testet Verzeichnis-Hinzufügen."""
  491. from trixy_core.plugins.hotreload import FileWatcher
  492. with tempfile.TemporaryDirectory() as tmpdir:
  493. watcher = FileWatcher()
  494. result = watcher.add_directory(Path(tmpdir))
  495. assert result
  496. assert Path(tmpdir) in watcher.watched_directories
  497. def test_add_invalid_directory(self):
  498. """Testet ungültiges Verzeichnis."""
  499. from trixy_core.plugins.hotreload import FileWatcher
  500. watcher = FileWatcher()
  501. result = watcher.add_directory(Path("/nonexistent/path"))
  502. assert not result
  503. def test_watch_callback(self):
  504. """Testet Callback-Registrierung."""
  505. from trixy_core.plugins.hotreload import FileWatcher
  506. watcher = FileWatcher()
  507. events = []
  508. @watcher.on_change
  509. def handler(event):
  510. events.append(event)
  511. assert len(watcher._callbacks) == 1
  512. class TestWatchEvent:
  513. """Tests für WatchEvent."""
  514. def test_event_creation(self):
  515. """Testet Event-Erstellung."""
  516. from trixy_core.plugins.hotreload import WatchEvent, WatchEventType
  517. event = WatchEvent(
  518. event_type=WatchEventType.MODIFIED,
  519. path=Path("/test/file.py"),
  520. )
  521. assert event.event_type == WatchEventType.MODIFIED
  522. assert event.path == Path("/test/file.py")
  523. def test_event_to_dict(self):
  524. """Testet Event-Serialisierung."""
  525. from trixy_core.plugins.hotreload import WatchEvent, WatchEventType
  526. event = WatchEvent(
  527. event_type=WatchEventType.CREATED,
  528. path=Path("/test/file.py"),
  529. )
  530. data = event.to_dict()
  531. assert data["event_type"] == "CREATED"
  532. assert "/test/file.py" in data["path"]
  533. class TestReloadConfig:
  534. """Tests für ReloadConfig."""
  535. def test_default_config(self):
  536. """Testet Standard-Konfiguration."""
  537. from trixy_core.plugins.hotreload import ReloadConfig
  538. config = ReloadConfig()
  539. assert config.enabled
  540. assert config.poll_interval == 1.0
  541. assert ".py" in config.extensions
  542. def test_custom_config(self):
  543. """Testet benutzerdefinierte Konfiguration."""
  544. from trixy_core.plugins.hotreload import ReloadConfig
  545. config = ReloadConfig(
  546. enabled=False,
  547. poll_interval=0.5,
  548. extensions=[".py"],
  549. )
  550. assert not config.enabled
  551. assert config.poll_interval == 0.5
  552. # ============================================================================
  553. # Testing Utilities Tests
  554. # ============================================================================
  555. class TestPluginTestHarness:
  556. """Tests für PluginTestHarness."""
  557. def test_create_harness(self):
  558. """Testet Harness-Erstellung."""
  559. from trixy_core.plugins.testing import PluginTestHarness
  560. harness = PluginTestHarness()
  561. assert harness.context is not None
  562. assert not harness.is_running
  563. def test_context_events(self):
  564. """Testet Event-Aufzeichnung."""
  565. from trixy_core.plugins.testing import PluginTestHarness
  566. harness = PluginTestHarness()
  567. context = harness.context
  568. context.events.append({"name": "test_event", "data": {}})
  569. assert context.has_event("test_event")
  570. assert len(context.get_events_by_name("test_event")) == 1
  571. def test_context_logs(self):
  572. """Testet Log-Aufzeichnung."""
  573. from trixy_core.plugins.testing import PluginTestHarness
  574. harness = PluginTestHarness()
  575. context = harness.context
  576. context.logs.append({"level": "error", "message": "Test"})
  577. context.logs.append({"level": "info", "message": "Info"})
  578. errors = context.get_logs_by_level("error")
  579. assert len(errors) == 1
  580. def test_context_clear(self):
  581. """Testet Kontext-Bereinigung."""
  582. from trixy_core.plugins.testing import PluginTestHarness
  583. harness = PluginTestHarness()
  584. context = harness.context
  585. context.events.append({"name": "test"})
  586. context.logs.append({"level": "info"})
  587. context.state["key"] = "value"
  588. context.clear_all()
  589. assert len(context.events) == 0
  590. assert len(context.logs) == 0
  591. assert len(context.state) == 0
  592. class TestMocks:
  593. """Tests für Mock-Objekte."""
  594. def test_mock_event_manager(self):
  595. """Testet MockEventManager."""
  596. from trixy_core.plugins.testing import MockEventManager
  597. manager = MockEventManager()
  598. event = manager.emit("test_event", {"key": "value"})
  599. assert event.name == "test_event"
  600. assert manager.has_event("test_event")
  601. assert manager.event_count() == 1
  602. def test_mock_satellite_manager(self):
  603. """Testet MockSatelliteManager."""
  604. from trixy_core.plugins.testing import MockSatelliteManager
  605. manager = MockSatelliteManager()
  606. manager.add_satellite("room1", {"alias": "Wohnzimmer"})
  607. assert manager.get("room1") is not None
  608. assert not manager.is_connected("room1")
  609. manager.connect("room1")
  610. assert manager.is_connected("room1")
  611. def test_mock_application(self):
  612. """Testet MockApplication."""
  613. from trixy_core.plugins.testing import MockApplication
  614. app = MockApplication()
  615. app.set_config("test_key", "test_value")
  616. assert app.get_config("test_key") == "test_value"
  617. event = app.emit("test_event")
  618. assert event.name == "test_event"
  619. def test_mock_plugin(self):
  620. """Testet MockPlugin."""
  621. from trixy_core.plugins.testing import MockPlugin
  622. plugin = MockPlugin("test_plugin")
  623. assert plugin.name == "test_plugin"
  624. assert plugin.enabled
  625. plugin.set_state("key", "value")
  626. state = plugin.get_state()
  627. assert state["key"] == "value"
  628. class TestPluginTestFixture:
  629. """Tests für PluginTestFixture."""
  630. def test_create_plugin(self):
  631. """Testet Plugin-Erstellung."""
  632. from trixy_core.plugins.testing import PluginTestFixture
  633. with PluginTestFixture() as fixture:
  634. plugin_dir = fixture.create_plugin("test_plugin")
  635. assert plugin_dir.exists()
  636. assert (plugin_dir / "main.py").exists()
  637. assert (plugin_dir / "config.json").exists()
  638. def test_multiple_plugins(self):
  639. """Testet mehrere Plugins."""
  640. from trixy_core.plugins.testing import PluginTestFixture
  641. with PluginTestFixture() as fixture:
  642. fixture.create_plugin("plugin_a")
  643. fixture.create_plugin("plugin_b")
  644. assert fixture.plugin_exists("plugin_a")
  645. assert fixture.plugin_exists("plugin_b")
  646. def test_add_file(self):
  647. """Testet Datei-Hinzufügen."""
  648. from trixy_core.plugins.testing import PluginTestFixture
  649. with PluginTestFixture() as fixture:
  650. fixture.create_plugin("test_plugin")
  651. path = fixture.add_file("test_plugin", "extra.py", "# Extra")
  652. assert path is not None
  653. assert path.exists()
  654. assert path.read_text() == "# Extra"
  655. def test_update_manifest(self):
  656. """Testet Manifest-Update."""
  657. from trixy_core.plugins.testing import PluginTestFixture
  658. with PluginTestFixture() as fixture:
  659. fixture.create_plugin("test_plugin")
  660. fixture.update_manifest("test_plugin", {"version": "2.0.0"})
  661. manifest = fixture.read_manifest("test_plugin")
  662. assert manifest["version"] == "2.0.0"
  663. class TestFixtureFunctions:
  664. """Tests für Fixture-Hilfsfunktionen."""
  665. def test_create_test_manifest(self):
  666. """Testet Manifest-Erstellung."""
  667. from trixy_core.plugins.testing import create_test_manifest
  668. manifest = create_test_manifest("my_plugin", version="2.0.0")
  669. assert manifest["name"] == "my_plugin"
  670. assert manifest["version"] == "2.0.0"
  671. def test_create_test_plugin_dir(self):
  672. """Testet Plugin-Verzeichnis-Erstellung."""
  673. from trixy_core.plugins.testing import (
  674. create_test_plugin_dir,
  675. cleanup_test_dir,
  676. )
  677. plugin_dir = create_test_plugin_dir("test_plugin")
  678. try:
  679. assert plugin_dir.exists()
  680. assert (plugin_dir / "main.py").exists()
  681. assert (plugin_dir / "config.json").exists()
  682. finally:
  683. cleanup_test_dir(plugin_dir.parent)
  684. # ============================================================================
  685. # Integration Tests
  686. # ============================================================================
  687. class TestPluginIntegration:
  688. """Integrationstests für das Plugin-System."""
  689. def test_complete_workflow(self):
  690. """Testet den kompletten Workflow."""
  691. from trixy_core.plugins.testing import PluginTestFixture
  692. from trixy_core.plugins.registry import PluginDiscovery, PluginRegistry
  693. from trixy_core.plugins.dependencies import DependencyResolver
  694. from trixy_core.plugins.hooks import HookRegistry
  695. with PluginTestFixture() as fixture:
  696. # Plugins erstellen
  697. fixture.create_plugin("core_plugin")
  698. fixture.create_plugin(
  699. "feature_plugin",
  700. manifest_data={
  701. "dependencies": {"core_plugin": "^1.0.0"},
  702. },
  703. )
  704. # Discovery
  705. discovery = PluginDiscovery()
  706. results = discovery.scan(fixture.base_dir)
  707. assert len(results) == 2
  708. # Registry
  709. registry = PluginRegistry()
  710. for result in results:
  711. registry.register(result.manifest)
  712. # Dependency Resolution
  713. resolver = DependencyResolver()
  714. resolver.register_plugin("core_plugin", "1.0.0", {})
  715. resolver.register_plugin("feature_plugin", "1.0.0", {"core_plugin": "^1.0.0"})
  716. resolution = resolver.resolve(["feature_plugin"])
  717. assert resolution.success
  718. # Hooks
  719. hook_registry = HookRegistry()
  720. hook_registry.register_handler(
  721. "plugin_loaded",
  722. "core_plugin",
  723. lambda name: f"Plugin {name} geladen",
  724. )
  725. result = hook_registry.trigger("plugin_loaded", "feature_plugin")
  726. assert len(result) == 1
  727. def test_version_compatibility_check(self):
  728. """Testet Versionskompatibilitätsprüfung."""
  729. from trixy_core.plugins.versioning import (
  730. VersionChecker,
  731. CompatibilityLevel,
  732. )
  733. from trixy_core.plugins.dependencies import DependencyResolver
  734. checker = VersionChecker()
  735. resolver = DependencyResolver()
  736. # Plugin mit Abhängigkeit registrieren
  737. resolver.register_plugin("base", "1.5.0", {})
  738. resolver.register_plugin("dependent", "1.0.0", {"base": "^1.0.0"})
  739. # Auflösung sollte funktionieren
  740. result = resolver.resolve(["dependent"])
  741. assert result.success
  742. # Kompatibilität prüfen
  743. compat = checker.check("^1.0.0", "1.5.0")
  744. assert compat.compatible
  745. if __name__ == "__main__":
  746. pytest.main([__file__, "-v"])