diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index 7c314cd..478670b 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -996,7 +996,9 @@ class PluginSystemManager: try: logger.info(f"Activating plugin: {module_name}") - self.plugin_manager.hook.activate(plugin=plugin_instance) + # Lifecycle is per-plugin. Broadcasting through pluggy replays + # activate() on every previously-registered plugin. + plugin_instance.activate(plugin_instance) except Exception as e: logger.error(f"Error activating plugin {module_name}: {e}") import traceback @@ -1035,7 +1037,9 @@ class PluginSystemManager: plugin_instance = pluginInfo.instance if plugin_instance: try: - self.plugin_manager.hook.deactivate(plugin=plugin_instance) + # Mirror targeted activation and only deactivate the plugin + # instance being unloaded. + plugin_instance.deactivate(plugin_instance) except Exception as e: logger.error(f"Error deactivating plugin {module_name}: {e}") diff --git a/tests/test_plugin_system_manager_regressions.py b/tests/test_plugin_system_manager_regressions.py new file mode 100644 index 0000000..f14db33 --- /dev/null +++ b/tests/test_plugin_system_manager_regressions.py @@ -0,0 +1,98 @@ +import sys +import tempfile +import textwrap +import types +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +input_event_manager_stub = types.ModuleType("cthulhu.input_event_manager") +input_event_manager_stub.get_manager = mock.Mock(return_value=mock.Mock()) +sys.modules["cthulhu.input_event_manager"] = input_event_manager_stub + +from cthulhu.plugin_system_manager import PluginInfo, PluginSystemManager + + +PLUGIN_TEMPLATE = """ +from cthulhu.plugin import Plugin, cthulhu_hookimpl + + +class {class_name}(Plugin): + def __init__(self): + super().__init__() + self.activation_count = 0 + self.deactivation_count = 0 + + @cthulhu_hookimpl + def activate(self, plugin=None): + self.activation_count += 1 + + @cthulhu_hookimpl + def deactivate(self, plugin=None): + self.deactivation_count += 1 +""" + + +class PluginSystemManagerRegressionTests(unittest.TestCase): + def _create_plugin_info(self, root_dir: str, module_name: str) -> PluginInfo: + plugin_dir = Path(root_dir) / module_name + plugin_dir.mkdir() + (plugin_dir / "plugin.py").write_text( + textwrap.dedent(PLUGIN_TEMPLATE.format(class_name=module_name)), + encoding="utf-8", + ) + return PluginInfo( + module_name, + module_name, + str(plugin_dir), + canonical_name=module_name, + source_id="test", + origin="test", + ) + + def _create_manager(self) -> PluginSystemManager: + app = mock.Mock() + app.getSignalManager.return_value = mock.Mock() + return PluginSystemManager(app) + + @mock.patch("cthulhu.plugin_system_manager.dbus_service.get_remote_controller") + def test_loading_second_plugin_does_not_reactivate_first_plugin(self, remote_controller): + remote_controller.return_value = mock.Mock() + + with tempfile.TemporaryDirectory() as temp_dir: + manager = self._create_manager() + first_plugin = self._create_plugin_info(temp_dir, "FirstPlugin") + second_plugin = self._create_plugin_info(temp_dir, "SecondPlugin") + + self.assertTrue(manager.loadPlugin(first_plugin)) + self.assertEqual(first_plugin.instance.activation_count, 1) + + self.assertTrue(manager.loadPlugin(second_plugin)) + + self.assertEqual(first_plugin.instance.activation_count, 1) + self.assertEqual(second_plugin.instance.activation_count, 1) + + @mock.patch("cthulhu.plugin_system_manager.dbus_service.get_remote_controller") + def test_unloading_plugin_does_not_deactivate_other_loaded_plugins(self, remote_controller): + remote_controller.return_value = mock.Mock() + + with tempfile.TemporaryDirectory() as temp_dir: + manager = self._create_manager() + first_plugin = self._create_plugin_info(temp_dir, "FirstPlugin") + second_plugin = self._create_plugin_info(temp_dir, "SecondPlugin") + + self.assertTrue(manager.loadPlugin(first_plugin)) + self.assertTrue(manager.loadPlugin(second_plugin)) + + second_instance = second_plugin.instance + + self.assertTrue(manager.unloadPlugin(second_plugin)) + + self.assertEqual(first_plugin.instance.deactivation_count, 0) + self.assertEqual(second_instance.deactivation_count, 1) + + +if __name__ == "__main__": + unittest.main()