# -*- coding: utf-8 -*- """ Test-Fixtures für Plugin-Tests. Hilfsfunktionen zum Erstellen von Test-Umgebungen. """ from __future__ import annotations import json import shutil import tempfile from dataclasses import dataclass, field from pathlib import Path from typing import Any def create_test_plugin_dir( name: str, base_dir: Path | None = None, with_manifest: bool = True, with_main: bool = True, manifest_data: dict[str, Any] | None = None, main_content: str | None = None, ) -> Path: """ Erstellt ein Test-Plugin-Verzeichnis. Args: name: Plugin-Name base_dir: Basis-Verzeichnis (Standard: temp) with_manifest: config.json erstellen with_main: main.py erstellen manifest_data: Manifest-Daten main_content: Inhalt für main.py Returns: Pfad zum Plugin-Verzeichnis """ if base_dir is None: base_dir = Path(tempfile.mkdtemp(prefix="trixy_plugin_test_")) plugin_dir = base_dir / name plugin_dir.mkdir(parents=True, exist_ok=True) if with_manifest: manifest = create_test_manifest( name=name, data=manifest_data, ) manifest_path = plugin_dir / "config.json" with open(manifest_path, "w", encoding="utf-8") as f: json.dump(manifest, f, indent=2) if with_main: main_path = plugin_dir / "main.py" content = main_content or _generate_default_main(name) main_path.write_text(content, encoding="utf-8") # __init__.py erstellen init_path = plugin_dir / "__init__.py" init_path.write_text("# -*- coding: utf-8 -*-\n", encoding="utf-8") return plugin_dir def create_test_manifest( name: str, version: str = "1.0.0", description: str | None = None, author: str = "Test", data: dict[str, Any] | None = None, ) -> dict[str, Any]: """ Erstellt ein Test-Manifest. Args: name: Plugin-Name version: Plugin-Version description: Beschreibung author: Autor data: Zusätzliche Daten Returns: Manifest-Dictionary """ manifest = { "name": name, "version": version, "description": description or f"Test-Plugin {name}", "author": author, "enabled": True, "dependencies": {}, "capabilities": [], "config": {}, } if data: manifest.update(data) return manifest def cleanup_test_dir(path: Path) -> bool: """ Löscht ein Test-Verzeichnis. Args: path: Zu löschendes Verzeichnis Returns: True bei Erfolg """ try: if path.exists(): shutil.rmtree(path) return True except Exception: return False def _generate_default_main(name: str) -> str: """Generiert eine Standard-main.py für Tests.""" class_name = "".join(word.capitalize() for word in name.split("_")) return f'''# -*- coding: utf-8 -*- """ Test-Plugin: {name} """ from trixy_core.plugins.base import TrixyPlugin class {class_name}Plugin(TrixyPlugin): """Test-Plugin-Implementierung.""" NAME = "{name}" VERSION = "1.0.0" DESCRIPTION = "Test-Plugin für {name}" AUTHOR = "Test" async def on_start(self) -> None: """Startet das Plugin.""" pass async def on_stop(self) -> None: """Stoppt das Plugin.""" pass # Plugin-Klasse für den Loader Plugin = {class_name}Plugin ''' @dataclass class PluginTestFixture: """ Fixture für Plugin-Tests. Verwaltet temporäre Test-Verzeichnisse und bietet Hilfsmethoden für Plugin-Tests. Example: # Als Context-Manager verwenden with PluginTestFixture() as fixture: plugin_dir = fixture.create_plugin("my_plugin") # Tests durchführen... # Aufräumung erfolgt automatisch # Oder manuell fixture = PluginTestFixture() fixture.setup() try: plugin_dir = fixture.create_plugin("my_plugin") # Tests... finally: fixture.teardown() """ base_dir: Path | None = None plugins: dict[str, Path] = field(default_factory=dict) _temp_dir: Path | None = field(default=None, repr=False) _cleanup: bool = field(default=True, repr=False) def __enter__(self) -> "PluginTestFixture": """Context-Manager-Eintritt.""" self.setup() return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: """Context-Manager-Austritt.""" self.teardown() def setup(self) -> None: """Initialisiert die Fixture.""" if self.base_dir is None: self._temp_dir = Path(tempfile.mkdtemp(prefix="trixy_test_fixture_")) self.base_dir = self._temp_dir else: self.base_dir.mkdir(parents=True, exist_ok=True) def teardown(self) -> None: """Räumt die Fixture auf.""" if self._cleanup and self._temp_dir and self._temp_dir.exists(): cleanup_test_dir(self._temp_dir) self.plugins.clear() def create_plugin( self, name: str, manifest_data: dict[str, Any] | None = None, main_content: str | None = None, with_manifest: bool = True, with_main: bool = True, ) -> Path: """ Erstellt ein Test-Plugin. Args: name: Plugin-Name manifest_data: Optionale Manifest-Daten main_content: Optionaler main.py-Inhalt with_manifest: config.json erstellen with_main: main.py erstellen Returns: Pfad zum Plugin-Verzeichnis """ if self.base_dir is None: self.setup() plugin_dir = create_test_plugin_dir( name=name, base_dir=self.base_dir, with_manifest=with_manifest, with_main=with_main, manifest_data=manifest_data, main_content=main_content, ) self.plugins[name] = plugin_dir return plugin_dir def get_plugin_path(self, name: str) -> Path | None: """Gibt den Pfad eines Plugins zurück.""" return self.plugins.get(name) def plugin_exists(self, name: str) -> bool: """Prüft, ob ein Plugin existiert.""" path = self.plugins.get(name) return path is not None and path.exists() def add_file( self, plugin_name: str, filename: str, content: str, ) -> Path | None: """ Fügt eine Datei zu einem Plugin hinzu. Args: plugin_name: Plugin-Name filename: Dateiname content: Dateiinhalt Returns: Pfad zur Datei oder None """ plugin_dir = self.plugins.get(plugin_name) if plugin_dir is None: return None file_path = plugin_dir / filename file_path.parent.mkdir(parents=True, exist_ok=True) file_path.write_text(content, encoding="utf-8") return file_path def add_json_file( self, plugin_name: str, filename: str, data: dict[str, Any], ) -> Path | None: """ Fügt eine JSON-Datei zu einem Plugin hinzu. Args: plugin_name: Plugin-Name filename: Dateiname data: JSON-Daten Returns: Pfad zur Datei oder None """ content = json.dumps(data, indent=2, ensure_ascii=False) return self.add_file(plugin_name, filename, content) def update_manifest( self, plugin_name: str, data: dict[str, Any], ) -> bool: """ Aktualisiert das Manifest eines Plugins. Args: plugin_name: Plugin-Name data: Zu aktualisierende Daten Returns: True bei Erfolg """ plugin_dir = self.plugins.get(plugin_name) if plugin_dir is None: return False manifest_path = plugin_dir / "config.json" if not manifest_path.exists(): return False try: with open(manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f) manifest.update(data) with open(manifest_path, "w", encoding="utf-8") as f: json.dump(manifest, f, indent=2) return True except Exception: return False def read_manifest(self, plugin_name: str) -> dict[str, Any] | None: """ Liest das Manifest eines Plugins. Args: plugin_name: Plugin-Name Returns: Manifest-Daten oder None """ plugin_dir = self.plugins.get(plugin_name) if plugin_dir is None: return None manifest_path = plugin_dir / "config.json" if not manifest_path.exists(): return None try: with open(manifest_path, "r", encoding="utf-8") as f: return json.load(f) except Exception: return None def remove_plugin(self, name: str) -> bool: """ Entfernt ein Plugin. Args: name: Plugin-Name Returns: True bei Erfolg """ plugin_dir = self.plugins.pop(name, None) if plugin_dir: return cleanup_test_dir(plugin_dir) return False def clear(self) -> None: """Entfernt alle Plugins.""" for name in list(self.plugins.keys()): self.remove_plugin(name) def create_mock_plugin_class( name: str, version: str = "1.0.0", handlers: dict[str, Any] | None = None, ) -> type: """ Erstellt eine Mock-Plugin-Klasse dynamisch. Args: name: Plugin-Name version: Plugin-Version handlers: Event-Handler als Dict Returns: Die Plugin-Klasse """ from trixy_core.plugins.testing.mocks import MockPlugin class_name = "".join(word.capitalize() for word in name.split("_")) + "Plugin" attrs = { "NAME": name, "VERSION": version, "DESCRIPTION": f"Dynamisch erstelltes Mock-Plugin: {name}", } if handlers: for event_name, handler in handlers.items(): method_name = f"on_{event_name.replace('.', '_')}" attrs[method_name] = handler return type(class_name, (MockPlugin,), attrs) def create_plugin_with_dependencies( fixture: PluginTestFixture, name: str, dependencies: dict[str, str], **kwargs: Any, ) -> Path: """ Erstellt ein Plugin mit Abhängigkeiten. Args: fixture: Test-Fixture name: Plugin-Name dependencies: Dict von Abhängigkeits-Name zu Version-Range **kwargs: Weitere Argumente für create_plugin Returns: Pfad zum Plugin-Verzeichnis """ manifest_data = kwargs.pop("manifest_data", {}) manifest_data["dependencies"] = dependencies return fixture.create_plugin( name=name, manifest_data=manifest_data, **kwargs, ) def create_plugin_chain( fixture: PluginTestFixture, names: list[str], ) -> list[Path]: """ Erstellt eine Kette von abhängigen Plugins. Jedes Plugin hängt vom vorherigen ab. Args: fixture: Test-Fixture names: Liste von Plugin-Namen Returns: Liste der Plugin-Pfade """ paths = [] for i, name in enumerate(names): dependencies = {} if i > 0: dependencies[names[i - 1]] = "^1.0.0" path = create_plugin_with_dependencies( fixture=fixture, name=name, dependencies=dependencies, ) paths.append(path) return paths