452 lines
14 KiB
Python
452 lines
14 KiB
Python
"""
|
|
Unit tests for Plugin Manager service.
|
|
|
|
Tests cover:
|
|
- Plugin discovery
|
|
- Plugin loading/unloading
|
|
- Plugin configuration management
|
|
- Enable/disable functionality
|
|
- Hotkey handling
|
|
"""
|
|
import sys
|
|
import unittest
|
|
import json
|
|
import tempfile
|
|
import shutil
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, mock_open
|
|
|
|
# Add project root to path
|
|
project_root = Path(__file__).parent.parent.parent
|
|
if str(project_root) not in sys.path:
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
# Mock BasePlugin for tests
|
|
class MockBasePlugin:
|
|
"""Mock BasePlugin class for testing."""
|
|
name = "TestPlugin"
|
|
version = "1.0.0"
|
|
description = "A test plugin"
|
|
hotkey = None
|
|
enabled = True
|
|
|
|
def __init__(self, overlay, config=None):
|
|
self.overlay = overlay
|
|
self.config = config or {}
|
|
|
|
def initialize(self):
|
|
pass
|
|
|
|
def shutdown(self):
|
|
pass
|
|
|
|
def get_ui(self):
|
|
return None
|
|
|
|
def on_hotkey(self):
|
|
pass
|
|
|
|
|
|
from core.plugin_manager import PluginManager
|
|
|
|
|
|
class TestPluginManagerInitialization(unittest.TestCase):
|
|
"""Test PluginManager initialization."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.overlay = MagicMock()
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_initialization_creates_empty_dicts(self):
|
|
"""Test that initialization creates empty plugin dicts."""
|
|
with patch.object(Path, 'exists', return_value=False):
|
|
pm = PluginManager(self.overlay)
|
|
|
|
self.assertEqual(pm.plugins, {})
|
|
self.assertEqual(pm.plugin_classes, {})
|
|
self.assertEqual(pm.overlay, self.overlay)
|
|
|
|
def test_initialization_loads_config(self):
|
|
"""Test that initialization loads configuration."""
|
|
config = {"enabled": ["plugin1"], "settings": {"plugin1": {"key": "value"}}}
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
json.dump(config, f)
|
|
config_path = f.name
|
|
|
|
try:
|
|
with patch.object(PluginManager, '_load_config', return_value=config):
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = config
|
|
|
|
self.assertEqual(pm.config["enabled"], ["plugin1"])
|
|
finally:
|
|
Path(config_path).unlink()
|
|
|
|
|
|
class TestPluginConfiguration(unittest.TestCase):
|
|
"""Test plugin configuration management."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.overlay = MagicMock()
|
|
self.config_path = Path(self.temp_dir) / "config" / "plugins.json"
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_load_config_default(self):
|
|
"""Test loading default config when file doesn't exist."""
|
|
with patch.object(PluginManager, '_load_config', return_value={"enabled": [], "settings": {}}):
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {"enabled": [], "settings": {}}
|
|
|
|
self.assertEqual(pm.config["enabled"], [])
|
|
self.assertEqual(pm.config["settings"], {})
|
|
|
|
def test_load_config_existing(self):
|
|
"""Test loading existing config file."""
|
|
config = {"enabled": ["plugin1", "plugin2"], "settings": {"plugin1": {"opt": 1}}}
|
|
|
|
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(self.config_path, 'w') as f:
|
|
json.dump(config, f)
|
|
|
|
with patch('core.plugin_manager.Path') as mock_path:
|
|
mock_path.return_value = self.config_path
|
|
mock_path.exists.return_value = True
|
|
|
|
pm = PluginManager(self.overlay)
|
|
# Manually load config
|
|
pm.config = json.loads(self.config_path.read_text())
|
|
|
|
self.assertEqual(pm.config["enabled"], ["plugin1", "plugin2"])
|
|
|
|
def test_is_plugin_enabled(self):
|
|
"""Test checking if plugin is enabled."""
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {"enabled": ["plugin1"]}
|
|
|
|
self.assertTrue(pm.is_plugin_enabled("plugin1"))
|
|
self.assertFalse(pm.is_plugin_enabled("plugin2"))
|
|
|
|
def test_is_plugin_enabled_empty_config(self):
|
|
"""Test checking if plugin is enabled with empty config."""
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {}
|
|
|
|
self.assertFalse(pm.is_plugin_enabled("plugin1"))
|
|
|
|
@patch.object(PluginManager, 'save_config')
|
|
def test_enable_plugin(self, mock_save):
|
|
"""Test enabling a plugin."""
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {"enabled": []}
|
|
pm.plugin_classes = {"test_plugin": MockBasePlugin}
|
|
|
|
with patch.object(pm, 'load_plugin', return_value=True):
|
|
result = pm.enable_plugin("test_plugin")
|
|
|
|
self.assertTrue(result)
|
|
self.assertIn("test_plugin", pm.config["enabled"])
|
|
mock_save.assert_called_once()
|
|
|
|
@patch.object(PluginManager, 'save_config')
|
|
def test_disable_plugin(self, mock_save):
|
|
"""Test disabling a plugin."""
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {"enabled": ["test_plugin"]}
|
|
pm.plugins = {}
|
|
|
|
with patch.object(pm, 'unload_plugin'):
|
|
result = pm.disable_plugin("test_plugin")
|
|
|
|
self.assertTrue(result)
|
|
self.assertNotIn("test_plugin", pm.config["enabled"])
|
|
mock_save.assert_called_once()
|
|
|
|
|
|
class TestPluginDiscovery(unittest.TestCase):
|
|
"""Test plugin discovery."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.overlay = MagicMock()
|
|
|
|
def test_discover_plugins_empty_dirs(self):
|
|
"""Test discovering plugins when directories are empty."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
pm = PluginManager(self.overlay)
|
|
pm.PLUGIN_DIRS = [tmpdir]
|
|
|
|
discovered = pm.discover_plugins()
|
|
|
|
self.assertEqual(discovered, [])
|
|
|
|
def test_discover_plugins_skips_pycache(self):
|
|
"""Test that discovery skips __pycache__ directories."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create __pycache__ directory
|
|
pycache = Path(tmpdir) / "__pycache__"
|
|
pycache.mkdir()
|
|
|
|
pm = PluginManager(self.overlay)
|
|
pm.PLUGIN_DIRS = [tmpdir]
|
|
|
|
discovered = pm.discover_plugins()
|
|
|
|
self.assertEqual(discovered, [])
|
|
|
|
def test_discover_plugins_skips_hidden(self):
|
|
"""Test that discovery skips hidden directories."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
# Create hidden directory
|
|
hidden = Path(tmpdir) / ".hidden"
|
|
hidden.mkdir()
|
|
|
|
pm = PluginManager(self.overlay)
|
|
pm.PLUGIN_DIRS = [tmpdir]
|
|
|
|
discovered = pm.discover_plugins()
|
|
|
|
self.assertEqual(discovered, [])
|
|
|
|
|
|
class TestPluginLoading(unittest.TestCase):
|
|
"""Test plugin loading."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.overlay = MagicMock()
|
|
self.pm = PluginManager(self.overlay)
|
|
|
|
def test_load_plugin_success(self):
|
|
"""Test successful plugin loading."""
|
|
self.pm.config = {"settings": {}}
|
|
|
|
result = self.pm.load_plugin(MockBasePlugin)
|
|
|
|
self.assertTrue(result)
|
|
self.assertEqual(len(self.pm.plugins), 1)
|
|
|
|
def test_load_plugin_already_loaded(self):
|
|
"""Test loading already loaded plugin."""
|
|
self.pm.config = {"settings": {}}
|
|
|
|
# Load once
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
|
|
# Try to load again
|
|
result = self.pm.load_plugin(MockBasePlugin)
|
|
|
|
self.assertTrue(result)
|
|
self.assertEqual(len(self.pm.plugins), 1) # Still only one
|
|
|
|
def test_load_plugin_disabled(self):
|
|
"""Test loading disabled plugin."""
|
|
plugin_id = f"{MockBasePlugin.__module__}.{MockBasePlugin.__name__}"
|
|
self.pm.config = {"disabled": [plugin_id]}
|
|
|
|
result = self.pm.load_plugin(MockBasePlugin)
|
|
|
|
self.assertFalse(result)
|
|
|
|
def test_load_plugin_init_failure(self):
|
|
"""Test loading plugin that fails initialization."""
|
|
class BadPlugin(MockBasePlugin):
|
|
def initialize(self):
|
|
raise Exception("Init failed")
|
|
|
|
self.pm.config = {"settings": {}}
|
|
|
|
result = self.pm.load_plugin(BadPlugin)
|
|
|
|
self.assertFalse(result)
|
|
|
|
def test_get_plugin(self):
|
|
"""Test getting a loaded plugin."""
|
|
self.pm.config = {"settings": {}}
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
|
|
plugin_id = f"{MockBasePlugin.__module__}.{MockBasePlugin.__name__}"
|
|
plugin = self.pm.get_plugin(plugin_id)
|
|
|
|
self.assertIsNotNone(plugin)
|
|
self.assertIsInstance(plugin, MockBasePlugin)
|
|
|
|
def test_get_plugin_not_loaded(self):
|
|
"""Test getting a plugin that isn't loaded."""
|
|
plugin = self.pm.get_plugin("nonexistent")
|
|
|
|
self.assertIsNone(plugin)
|
|
|
|
def test_get_all_plugins(self):
|
|
"""Test getting all loaded plugins."""
|
|
self.pm.config = {"settings": {}}
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
|
|
all_plugins = self.pm.get_all_plugins()
|
|
|
|
self.assertEqual(len(all_plugins), 1)
|
|
|
|
|
|
class TestPluginUnloading(unittest.TestCase):
|
|
"""Test plugin unloading."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.overlay = MagicMock()
|
|
self.pm = PluginManager(self.overlay)
|
|
self.pm.config = {"settings": {}}
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
self.plugin_id = f"{MockBasePlugin.__module__}.{MockBasePlugin.__name__}"
|
|
|
|
def test_unload_plugin(self):
|
|
"""Test unloading a plugin."""
|
|
self.assertIn(self.plugin_id, self.pm.plugins)
|
|
|
|
self.pm.unload_plugin(self.plugin_id)
|
|
|
|
self.assertNotIn(self.plugin_id, self.pm.plugins)
|
|
|
|
def test_unload_plugin_calls_shutdown(self):
|
|
"""Test that unloading calls plugin shutdown."""
|
|
plugin = self.pm.plugins[self.plugin_id]
|
|
plugin.shutdown = MagicMock()
|
|
|
|
self.pm.unload_plugin(self.plugin_id)
|
|
|
|
plugin.shutdown.assert_called_once()
|
|
|
|
def test_unload_nonexistent_plugin(self):
|
|
"""Test unloading a plugin that doesn't exist."""
|
|
# Should not raise
|
|
self.pm.unload_plugin("nonexistent")
|
|
|
|
def test_shutdown_all(self):
|
|
"""Test shutting down all plugins."""
|
|
# Load another plugin
|
|
class AnotherPlugin(MockBasePlugin):
|
|
name = "AnotherPlugin"
|
|
|
|
self.pm.load_plugin(AnotherPlugin)
|
|
|
|
self.assertEqual(len(self.pm.plugins), 2)
|
|
|
|
self.pm.shutdown_all()
|
|
|
|
self.assertEqual(len(self.pm.plugins), 0)
|
|
|
|
|
|
class TestPluginUI(unittest.TestCase):
|
|
"""Test plugin UI functionality."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.overlay = MagicMock()
|
|
self.pm = PluginManager(self.overlay)
|
|
self.pm.config = {"settings": {}}
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
|
|
def test_get_plugin_ui(self):
|
|
"""Test getting plugin UI."""
|
|
plugin_id = f"{MockBasePlugin.__module__}.{MockBasePlugin.__name__}"
|
|
|
|
ui = self.pm.get_plugin_ui(plugin_id)
|
|
|
|
# MockBasePlugin.get_ui returns None
|
|
self.assertIsNone(ui)
|
|
|
|
def test_get_plugin_ui_not_loaded(self):
|
|
"""Test getting UI for unloaded plugin."""
|
|
ui = self.pm.get_plugin_ui("nonexistent")
|
|
|
|
self.assertIsNone(ui)
|
|
|
|
|
|
class TestHotkeyHandling(unittest.TestCase):
|
|
"""Test hotkey handling."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.overlay = MagicMock()
|
|
self.pm = PluginManager(self.overlay)
|
|
self.pm.config = {"settings": {}}
|
|
|
|
def test_trigger_hotkey_success(self):
|
|
"""Test triggering hotkey for a plugin."""
|
|
class HotkeyPlugin(MockBasePlugin):
|
|
hotkey = "ctrl+t"
|
|
on_hotkey = MagicMock()
|
|
|
|
self.pm.load_plugin(HotkeyPlugin)
|
|
|
|
result = self.pm.trigger_hotkey("ctrl+t")
|
|
|
|
self.assertTrue(result)
|
|
HotkeyPlugin.on_hotkey.assert_called_once()
|
|
|
|
def test_trigger_hotkey_not_handled(self):
|
|
"""Test triggering hotkey that no plugin handles."""
|
|
self.pm.load_plugin(MockBasePlugin)
|
|
|
|
result = self.pm.trigger_hotkey("ctrl+unknown")
|
|
|
|
self.assertFalse(result)
|
|
|
|
def test_trigger_hotkey_disabled_plugin(self):
|
|
"""Test that disabled plugins don't handle hotkeys."""
|
|
class DisabledPlugin(MockBasePlugin):
|
|
hotkey = "ctrl+d"
|
|
enabled = False
|
|
on_hotkey = MagicMock()
|
|
|
|
self.pm.load_plugin(DisabledPlugin)
|
|
|
|
result = self.pm.trigger_hotkey("ctrl+d")
|
|
|
|
self.assertFalse(result)
|
|
DisabledPlugin.on_hotkey.assert_not_called()
|
|
|
|
|
|
class TestPluginManagerSaveConfig(unittest.TestCase):
|
|
"""Test plugin manager configuration saving."""
|
|
|
|
def setUp(self):
|
|
"""Set up test environment."""
|
|
self.temp_dir = tempfile.mkdtemp()
|
|
self.overlay = MagicMock()
|
|
|
|
def tearDown(self):
|
|
"""Clean up test environment."""
|
|
shutil.rmtree(self.temp_dir)
|
|
|
|
def test_save_config(self):
|
|
"""Test saving configuration to file."""
|
|
config_path = Path(self.temp_dir) / "config" / "plugins.json"
|
|
|
|
pm = PluginManager(self.overlay)
|
|
pm.config = {"enabled": ["plugin1"], "settings": {"plugin1": {"key": "value"}}}
|
|
|
|
with patch('core.plugin_manager.Path') as mock_path_class:
|
|
mock_path = MagicMock()
|
|
mock_path.__truediv__ = MagicMock(return_value=mock_path)
|
|
mock_path.parent = MagicMock()
|
|
mock_path.exists.return_value = True
|
|
mock_path_class.return_value = mock_path
|
|
|
|
# Just verify config is serializable
|
|
config_json = json.dumps(pm.config, indent=2)
|
|
self.assertIn("plugin1", config_json)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|