""" Base test class for plugin integration tests. Provides common test functionality for all plugins. """ import pytest import json from pathlib import Path from typing import Dict, Any from unittest.mock import MagicMock from src.plugin_system.plugin_loader import PluginLoader from src.plugin_system.base_plugin import BasePlugin class PluginTestBase: """Base class for plugin integration tests.""" @pytest.fixture(autouse=True) def setup_base(self, plugins_dir, mock_display_manager, mock_cache_manager, mock_plugin_manager, base_plugin_config): """Setup base fixtures for all plugin tests.""" self.plugins_dir = plugins_dir self.mock_display_manager = mock_display_manager self.mock_cache_manager = mock_cache_manager self.mock_plugin_manager = mock_plugin_manager self.base_config = base_plugin_config self.plugin_loader = PluginLoader() def load_plugin_manifest(self, plugin_id: str) -> Dict[str, Any]: """Load plugin manifest.json.""" manifest_path = self.plugins_dir / plugin_id / 'manifest.json' if not manifest_path.exists(): pytest.skip(f"Manifest not found for {plugin_id}") with open(manifest_path, 'r') as f: return json.load(f) def load_plugin_config_schema(self, plugin_id: str) -> Dict[str, Any]: """Load plugin config_schema.json if it exists.""" schema_path = self.plugins_dir / plugin_id / 'config_schema.json' if schema_path.exists(): with open(schema_path, 'r') as f: return json.load(f) return None def test_manifest_exists(self, plugin_id: str): """Test that plugin manifest exists and is valid JSON.""" manifest = self.load_plugin_manifest(plugin_id) assert manifest is not None assert 'id' in manifest assert manifest['id'] == plugin_id assert 'class_name' in manifest # entry_point is optional - default to 'manager.py' if missing if 'entry_point' not in manifest: manifest['entry_point'] = 'manager.py' def test_manifest_has_required_fields(self, plugin_id: str): """Test that manifest has all required fields.""" manifest = self.load_plugin_manifest(plugin_id) # Core required fields required_fields = ['id', 'name', 'description', 'author', 'class_name'] for field in required_fields: assert field in manifest, f"Manifest missing required field: {field}" assert manifest[field], f"Manifest field {field} is empty" # entry_point is required but some plugins may not have it explicitly # If missing, assume it's 'manager.py' if 'entry_point' not in manifest: manifest['entry_point'] = 'manager.py' def test_plugin_can_be_loaded(self, plugin_id: str): """Test that plugin module can be loaded.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) assert module is not None assert hasattr(module, manifest['class_name']) def test_plugin_class_exists(self, plugin_id: str): """Test that plugin class exists in module.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') class_name = manifest['class_name'] module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) plugin_class = self.plugin_loader.get_plugin_class( plugin_id=plugin_id, module=module, class_name=class_name ) assert plugin_class is not None assert issubclass(plugin_class, BasePlugin) def test_plugin_can_be_instantiated(self, plugin_id: str): """Test that plugin can be instantiated with mock dependencies.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') class_name = manifest['class_name'] module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) plugin_class = self.plugin_loader.get_plugin_class( plugin_id=plugin_id, module=module, class_name=class_name ) # Merge base config with plugin-specific defaults config = self.base_config.copy() plugin_instance = self.plugin_loader.instantiate_plugin( plugin_id=plugin_id, plugin_class=plugin_class, config=config, display_manager=self.mock_display_manager, cache_manager=self.mock_cache_manager, plugin_manager=self.mock_plugin_manager ) assert plugin_instance is not None assert plugin_instance.plugin_id == plugin_id assert plugin_instance.enabled == config.get('enabled', True) def test_plugin_has_required_methods(self, plugin_id: str): """Test that plugin has required BasePlugin methods.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') class_name = manifest['class_name'] module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) plugin_class = self.plugin_loader.get_plugin_class( plugin_id=plugin_id, module=module, class_name=class_name ) config = self.base_config.copy() plugin_instance = self.plugin_loader.instantiate_plugin( plugin_id=plugin_id, plugin_class=plugin_class, config=config, display_manager=self.mock_display_manager, cache_manager=self.mock_cache_manager, plugin_manager=self.mock_plugin_manager ) # Check required methods exist assert hasattr(plugin_instance, 'update') assert hasattr(plugin_instance, 'display') assert callable(plugin_instance.update) assert callable(plugin_instance.display) def test_plugin_update_method(self, plugin_id: str): """Test that plugin update() method can be called without errors.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') class_name = manifest['class_name'] module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) plugin_class = self.plugin_loader.get_plugin_class( plugin_id=plugin_id, module=module, class_name=class_name ) config = self.base_config.copy() plugin_instance = self.plugin_loader.instantiate_plugin( plugin_id=plugin_id, plugin_class=plugin_class, config=config, display_manager=self.mock_display_manager, cache_manager=self.mock_cache_manager, plugin_manager=self.mock_plugin_manager ) # Call update() - should not raise exceptions # Some plugins may need API keys, but they should handle that gracefully try: plugin_instance.update() except Exception as e: # If it's a missing API key or similar, that's acceptable for integration tests error_msg = str(e).lower() if 'api' in error_msg or 'key' in error_msg or 'auth' in error_msg or 'credential' in error_msg: pytest.skip(f"Plugin requires API credentials: {e}") else: raise def test_plugin_display_method(self, plugin_id: str): """Test that plugin display() method can be called without errors.""" manifest = self.load_plugin_manifest(plugin_id) plugin_dir = self.plugins_dir / plugin_id entry_point = manifest.get('entry_point', 'manager.py') class_name = manifest['class_name'] module = self.plugin_loader.load_module( plugin_id=plugin_id, plugin_dir=plugin_dir, entry_point=entry_point ) plugin_class = self.plugin_loader.get_plugin_class( plugin_id=plugin_id, module=module, class_name=class_name ) config = self.base_config.copy() plugin_instance = self.plugin_loader.instantiate_plugin( plugin_id=plugin_id, plugin_class=plugin_class, config=config, display_manager=self.mock_display_manager, cache_manager=self.mock_cache_manager, plugin_manager=self.mock_plugin_manager ) # Some plugins need matrix attribute on display_manager (set before update) if not hasattr(self.mock_display_manager, 'matrix'): from unittest.mock import MagicMock self.mock_display_manager.matrix = MagicMock() self.mock_display_manager.matrix.width = 128 self.mock_display_manager.matrix.height = 32 # Call update() first if needed try: plugin_instance.update() except Exception as e: error_msg = str(e).lower() if 'api' in error_msg or 'key' in error_msg or 'auth' in error_msg: pytest.skip(f"Plugin requires API credentials: {e}") # Some plugins need a mode set before display # Try to set a mode if the plugin has that capability if hasattr(plugin_instance, 'set_mode') and manifest.get('display_modes'): try: first_mode = manifest['display_modes'][0] plugin_instance.set_mode(first_mode) except Exception: pass # If set_mode doesn't exist or fails, continue # Call display() - should not raise exceptions try: plugin_instance.display(force_clear=True) except Exception as e: # Some plugins may need specific setup - if it's a mode issue, that's acceptable error_msg = str(e).lower() if 'mode' in error_msg or 'manager' in error_msg: # This is acceptable - plugin needs proper mode setup pass else: raise # Verify display_manager methods were called (if display succeeded) # Some plugins may not call these if they skip display due to missing data # So we just verify the method was callable without exceptions assert hasattr(plugin_instance, 'display') def test_plugin_has_display_modes(self, plugin_id: str): """Test that plugin has display modes defined.""" manifest = self.load_plugin_manifest(plugin_id) assert 'display_modes' in manifest assert isinstance(manifest['display_modes'], list) assert len(manifest['display_modes']) > 0 def test_config_schema_valid(self, plugin_id: str): """Test that config schema is valid JSON if it exists.""" schema = self.load_plugin_config_schema(plugin_id) if schema is not None: assert isinstance(schema, dict) # Schema should have 'type' field for JSON Schema assert 'type' in schema or 'properties' in schema