fixtures.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. # -*- coding: utf-8 -*-
  2. """
  3. Test-Fixtures für Plugin-Tests.
  4. Hilfsfunktionen zum Erstellen von Test-Umgebungen.
  5. """
  6. from __future__ import annotations
  7. import json
  8. import shutil
  9. import tempfile
  10. from dataclasses import dataclass, field
  11. from pathlib import Path
  12. from typing import Any
  13. def create_test_plugin_dir(
  14. name: str,
  15. base_dir: Path | None = None,
  16. with_manifest: bool = True,
  17. with_main: bool = True,
  18. manifest_data: dict[str, Any] | None = None,
  19. main_content: str | None = None,
  20. ) -> Path:
  21. """
  22. Erstellt ein Test-Plugin-Verzeichnis.
  23. Args:
  24. name: Plugin-Name
  25. base_dir: Basis-Verzeichnis (Standard: temp)
  26. with_manifest: config.json erstellen
  27. with_main: main.py erstellen
  28. manifest_data: Manifest-Daten
  29. main_content: Inhalt für main.py
  30. Returns:
  31. Pfad zum Plugin-Verzeichnis
  32. """
  33. if base_dir is None:
  34. base_dir = Path(tempfile.mkdtemp(prefix="trixy_plugin_test_"))
  35. plugin_dir = base_dir / name
  36. plugin_dir.mkdir(parents=True, exist_ok=True)
  37. if with_manifest:
  38. manifest = create_test_manifest(
  39. name=name,
  40. data=manifest_data,
  41. )
  42. manifest_path = plugin_dir / "config.json"
  43. with open(manifest_path, "w", encoding="utf-8") as f:
  44. json.dump(manifest, f, indent=2)
  45. if with_main:
  46. main_path = plugin_dir / "main.py"
  47. content = main_content or _generate_default_main(name)
  48. main_path.write_text(content, encoding="utf-8")
  49. # __init__.py erstellen
  50. init_path = plugin_dir / "__init__.py"
  51. init_path.write_text("# -*- coding: utf-8 -*-\n", encoding="utf-8")
  52. return plugin_dir
  53. def create_test_manifest(
  54. name: str,
  55. version: str = "1.0.0",
  56. description: str | None = None,
  57. author: str = "Test",
  58. data: dict[str, Any] | None = None,
  59. ) -> dict[str, Any]:
  60. """
  61. Erstellt ein Test-Manifest.
  62. Args:
  63. name: Plugin-Name
  64. version: Plugin-Version
  65. description: Beschreibung
  66. author: Autor
  67. data: Zusätzliche Daten
  68. Returns:
  69. Manifest-Dictionary
  70. """
  71. manifest = {
  72. "name": name,
  73. "version": version,
  74. "description": description or f"Test-Plugin {name}",
  75. "author": author,
  76. "enabled": True,
  77. "dependencies": {},
  78. "capabilities": [],
  79. "config": {},
  80. }
  81. if data:
  82. manifest.update(data)
  83. return manifest
  84. def cleanup_test_dir(path: Path) -> bool:
  85. """
  86. Löscht ein Test-Verzeichnis.
  87. Args:
  88. path: Zu löschendes Verzeichnis
  89. Returns:
  90. True bei Erfolg
  91. """
  92. try:
  93. if path.exists():
  94. shutil.rmtree(path)
  95. return True
  96. except Exception:
  97. return False
  98. def _generate_default_main(name: str) -> str:
  99. """Generiert eine Standard-main.py für Tests."""
  100. class_name = "".join(word.capitalize() for word in name.split("_"))
  101. return f'''# -*- coding: utf-8 -*-
  102. """
  103. Test-Plugin: {name}
  104. """
  105. from trixy_core.plugins.base import TrixyPlugin
  106. class {class_name}Plugin(TrixyPlugin):
  107. """Test-Plugin-Implementierung."""
  108. NAME = "{name}"
  109. VERSION = "1.0.0"
  110. DESCRIPTION = "Test-Plugin für {name}"
  111. AUTHOR = "Test"
  112. async def on_start(self) -> None:
  113. """Startet das Plugin."""
  114. pass
  115. async def on_stop(self) -> None:
  116. """Stoppt das Plugin."""
  117. pass
  118. # Plugin-Klasse für den Loader
  119. Plugin = {class_name}Plugin
  120. '''
  121. @dataclass
  122. class PluginTestFixture:
  123. """
  124. Fixture für Plugin-Tests.
  125. Verwaltet temporäre Test-Verzeichnisse und bietet
  126. Hilfsmethoden für Plugin-Tests.
  127. Example:
  128. # Als Context-Manager verwenden
  129. with PluginTestFixture() as fixture:
  130. plugin_dir = fixture.create_plugin("my_plugin")
  131. # Tests durchführen...
  132. # Aufräumung erfolgt automatisch
  133. # Oder manuell
  134. fixture = PluginTestFixture()
  135. fixture.setup()
  136. try:
  137. plugin_dir = fixture.create_plugin("my_plugin")
  138. # Tests...
  139. finally:
  140. fixture.teardown()
  141. """
  142. base_dir: Path | None = None
  143. plugins: dict[str, Path] = field(default_factory=dict)
  144. _temp_dir: Path | None = field(default=None, repr=False)
  145. _cleanup: bool = field(default=True, repr=False)
  146. def __enter__(self) -> "PluginTestFixture":
  147. """Context-Manager-Eintritt."""
  148. self.setup()
  149. return self
  150. def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
  151. """Context-Manager-Austritt."""
  152. self.teardown()
  153. def setup(self) -> None:
  154. """Initialisiert die Fixture."""
  155. if self.base_dir is None:
  156. self._temp_dir = Path(tempfile.mkdtemp(prefix="trixy_test_fixture_"))
  157. self.base_dir = self._temp_dir
  158. else:
  159. self.base_dir.mkdir(parents=True, exist_ok=True)
  160. def teardown(self) -> None:
  161. """Räumt die Fixture auf."""
  162. if self._cleanup and self._temp_dir and self._temp_dir.exists():
  163. cleanup_test_dir(self._temp_dir)
  164. self.plugins.clear()
  165. def create_plugin(
  166. self,
  167. name: str,
  168. manifest_data: dict[str, Any] | None = None,
  169. main_content: str | None = None,
  170. with_manifest: bool = True,
  171. with_main: bool = True,
  172. ) -> Path:
  173. """
  174. Erstellt ein Test-Plugin.
  175. Args:
  176. name: Plugin-Name
  177. manifest_data: Optionale Manifest-Daten
  178. main_content: Optionaler main.py-Inhalt
  179. with_manifest: config.json erstellen
  180. with_main: main.py erstellen
  181. Returns:
  182. Pfad zum Plugin-Verzeichnis
  183. """
  184. if self.base_dir is None:
  185. self.setup()
  186. plugin_dir = create_test_plugin_dir(
  187. name=name,
  188. base_dir=self.base_dir,
  189. with_manifest=with_manifest,
  190. with_main=with_main,
  191. manifest_data=manifest_data,
  192. main_content=main_content,
  193. )
  194. self.plugins[name] = plugin_dir
  195. return plugin_dir
  196. def get_plugin_path(self, name: str) -> Path | None:
  197. """Gibt den Pfad eines Plugins zurück."""
  198. return self.plugins.get(name)
  199. def plugin_exists(self, name: str) -> bool:
  200. """Prüft, ob ein Plugin existiert."""
  201. path = self.plugins.get(name)
  202. return path is not None and path.exists()
  203. def add_file(
  204. self,
  205. plugin_name: str,
  206. filename: str,
  207. content: str,
  208. ) -> Path | None:
  209. """
  210. Fügt eine Datei zu einem Plugin hinzu.
  211. Args:
  212. plugin_name: Plugin-Name
  213. filename: Dateiname
  214. content: Dateiinhalt
  215. Returns:
  216. Pfad zur Datei oder None
  217. """
  218. plugin_dir = self.plugins.get(plugin_name)
  219. if plugin_dir is None:
  220. return None
  221. file_path = plugin_dir / filename
  222. file_path.parent.mkdir(parents=True, exist_ok=True)
  223. file_path.write_text(content, encoding="utf-8")
  224. return file_path
  225. def add_json_file(
  226. self,
  227. plugin_name: str,
  228. filename: str,
  229. data: dict[str, Any],
  230. ) -> Path | None:
  231. """
  232. Fügt eine JSON-Datei zu einem Plugin hinzu.
  233. Args:
  234. plugin_name: Plugin-Name
  235. filename: Dateiname
  236. data: JSON-Daten
  237. Returns:
  238. Pfad zur Datei oder None
  239. """
  240. content = json.dumps(data, indent=2, ensure_ascii=False)
  241. return self.add_file(plugin_name, filename, content)
  242. def update_manifest(
  243. self,
  244. plugin_name: str,
  245. data: dict[str, Any],
  246. ) -> bool:
  247. """
  248. Aktualisiert das Manifest eines Plugins.
  249. Args:
  250. plugin_name: Plugin-Name
  251. data: Zu aktualisierende Daten
  252. Returns:
  253. True bei Erfolg
  254. """
  255. plugin_dir = self.plugins.get(plugin_name)
  256. if plugin_dir is None:
  257. return False
  258. manifest_path = plugin_dir / "config.json"
  259. if not manifest_path.exists():
  260. return False
  261. try:
  262. with open(manifest_path, "r", encoding="utf-8") as f:
  263. manifest = json.load(f)
  264. manifest.update(data)
  265. with open(manifest_path, "w", encoding="utf-8") as f:
  266. json.dump(manifest, f, indent=2)
  267. return True
  268. except Exception:
  269. return False
  270. def read_manifest(self, plugin_name: str) -> dict[str, Any] | None:
  271. """
  272. Liest das Manifest eines Plugins.
  273. Args:
  274. plugin_name: Plugin-Name
  275. Returns:
  276. Manifest-Daten oder None
  277. """
  278. plugin_dir = self.plugins.get(plugin_name)
  279. if plugin_dir is None:
  280. return None
  281. manifest_path = plugin_dir / "config.json"
  282. if not manifest_path.exists():
  283. return None
  284. try:
  285. with open(manifest_path, "r", encoding="utf-8") as f:
  286. return json.load(f)
  287. except Exception:
  288. return None
  289. def remove_plugin(self, name: str) -> bool:
  290. """
  291. Entfernt ein Plugin.
  292. Args:
  293. name: Plugin-Name
  294. Returns:
  295. True bei Erfolg
  296. """
  297. plugin_dir = self.plugins.pop(name, None)
  298. if plugin_dir:
  299. return cleanup_test_dir(plugin_dir)
  300. return False
  301. def clear(self) -> None:
  302. """Entfernt alle Plugins."""
  303. for name in list(self.plugins.keys()):
  304. self.remove_plugin(name)
  305. def create_mock_plugin_class(
  306. name: str,
  307. version: str = "1.0.0",
  308. handlers: dict[str, Any] | None = None,
  309. ) -> type:
  310. """
  311. Erstellt eine Mock-Plugin-Klasse dynamisch.
  312. Args:
  313. name: Plugin-Name
  314. version: Plugin-Version
  315. handlers: Event-Handler als Dict
  316. Returns:
  317. Die Plugin-Klasse
  318. """
  319. from trixy_core.plugins.testing.mocks import MockPlugin
  320. class_name = "".join(word.capitalize() for word in name.split("_")) + "Plugin"
  321. attrs = {
  322. "NAME": name,
  323. "VERSION": version,
  324. "DESCRIPTION": f"Dynamisch erstelltes Mock-Plugin: {name}",
  325. }
  326. if handlers:
  327. for event_name, handler in handlers.items():
  328. method_name = f"on_{event_name.replace('.', '_')}"
  329. attrs[method_name] = handler
  330. return type(class_name, (MockPlugin,), attrs)
  331. def create_plugin_with_dependencies(
  332. fixture: PluginTestFixture,
  333. name: str,
  334. dependencies: dict[str, str],
  335. **kwargs: Any,
  336. ) -> Path:
  337. """
  338. Erstellt ein Plugin mit Abhängigkeiten.
  339. Args:
  340. fixture: Test-Fixture
  341. name: Plugin-Name
  342. dependencies: Dict von Abhängigkeits-Name zu Version-Range
  343. **kwargs: Weitere Argumente für create_plugin
  344. Returns:
  345. Pfad zum Plugin-Verzeichnis
  346. """
  347. manifest_data = kwargs.pop("manifest_data", {})
  348. manifest_data["dependencies"] = dependencies
  349. return fixture.create_plugin(
  350. name=name,
  351. manifest_data=manifest_data,
  352. **kwargs,
  353. )
  354. def create_plugin_chain(
  355. fixture: PluginTestFixture,
  356. names: list[str],
  357. ) -> list[Path]:
  358. """
  359. Erstellt eine Kette von abhängigen Plugins.
  360. Jedes Plugin hängt vom vorherigen ab.
  361. Args:
  362. fixture: Test-Fixture
  363. names: Liste von Plugin-Namen
  364. Returns:
  365. Liste der Plugin-Pfade
  366. """
  367. paths = []
  368. for i, name in enumerate(names):
  369. dependencies = {}
  370. if i > 0:
  371. dependencies[names[i - 1]] = "^1.0.0"
  372. path = create_plugin_with_dependencies(
  373. fixture=fixture,
  374. name=name,
  375. dependencies=dependencies,
  376. )
  377. paths.append(path)
  378. return paths