diff --git a/deluge/tests/common.py b/deluge/tests/common.py index af221de2d5..ae2f180e5b 100644 --- a/deluge/tests/common.py +++ b/deluge/tests/common.py @@ -9,6 +9,8 @@ import os import sys import traceback +from pathlib import Path +from unittest import mock import pytest import pytest_twisted @@ -16,9 +18,11 @@ from twisted.internet.defer import Deferred from twisted.internet.error import CannotListenError +import deluge.component as component import deluge.configmanager import deluge.core.preferencesmanager import deluge.log +import deluge.pluginmanagerbase from deluge.common import get_localhost_auth from deluge.error import DelugeError from deluge.ui.client import Client @@ -39,6 +43,14 @@ def get_test_data_file(filename): return os.path.join(os.path.join(os.path.dirname(__file__), 'data'), filename) +def plugin_search_dir(self): + basedir = get_test_data_file('') + test_plugin_wheels = list(Path(basedir).glob('*.whl')) + plugin_dir = [basedir] + [str(i) for i in test_plugin_wheels] + print(f'plugin_search_dir(): plugin_dir is: {plugin_dir}') + return plugin_dir + + def todo_test(caller): # If we are using the delugereporter we can set todo mark on the test # Without the delugereporter the todo would print a stack trace, so in @@ -66,6 +78,65 @@ def callback(value): return watchdog +@mock.patch('deluge.pluginmanagerbase.PluginManagerBase.get_plugin_dirs', plugin_search_dir) +@pytest.mark.usefixtures('config_dir') +class TestPluginManager( + deluge.pluginmanagerbase.PluginManagerBase, component.Component +): + """For testing the PluginManager and PluginResourceManager.""" + + def __init__(self, core): + component.Component.__init__(self, 'TestPluginManager') + + self.status_fields = {} + + # Call the PluginManagerBase constructor + deluge.pluginmanagerbase.PluginManagerBase.__init__( + self, 'core.conf', 'deluge.plugin.core' + ) + + def start(self): + # Enable plugins that are enabled in the config + self.enable_plugins() + + def stop(self): + # Disable all enabled plugins + self.disable_plugins() + + def shutdown(self): + self.stop() + + def update_plugins(self): + for plugin in self.plugins: + if hasattr(self.plugins[plugin], 'update'): + try: + self.plugins[plugin].update() + except Exception as ex: + deluge.log.critical(ex) + + def enable_plugin(self, name): + d = defer.succeed(True) + if name not in self.plugins: + d = deluge.pluginmanagerbase.PluginManagerBase.enable_plugin(self, name) + + def on_enable_plugin(result): + return result + + d.addBoth(on_enable_plugin) + return d + + def disable_plugin(self, name): + d = defer.succeed(True) + if name in self.plugins: + d = deluge.pluginmanagerbase.PluginManagerBase.disable_plugin(self, name) + + def on_disable_plugin(result): + return result + + d.addBoth(on_disable_plugin) + return d + + class ReactorOverride: """Class used to patch reactor while running unit tests to avoid starting and stopping the twisted reactor diff --git a/deluge/tests/test_plugin_metadata.py b/deluge/tests/test_plugin_metadata.py index 438b486a58..a3626b25ef 100644 --- a/deluge/tests/test_plugin_metadata.py +++ b/deluge/tests/test_plugin_metadata.py @@ -6,10 +6,19 @@ # See LICENSE for more details. # +from unittest import mock + from deluge.pluginmanagerbase import PluginManagerBase +from . import common + +@mock.patch('deluge.pluginmanagerbase.PluginManagerBase.get_plugin_dirs', common.plugin_search_dir) class TestPluginManagerBase: + def test_scan_for_plugins(self): + pm = PluginManagerBase('core.conf', 'deluge.plugin.core') + assert 'plugin_resources_test' in pm.available_plugins + def test_get_plugin_info(self): pm = PluginManagerBase('core.conf', 'deluge.plugin.core') for p in pm.get_available_plugins(): diff --git a/deluge/tests/test_plugin_resources.py b/deluge/tests/test_plugin_resources.py new file mode 100644 index 0000000000..7c530681eb --- /dev/null +++ b/deluge/tests/test_plugin_resources.py @@ -0,0 +1,122 @@ +# +# Copyright (C) 2024 Gregorio Litenstein +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import os +from unittest import mock + +import deluge.component as component +from deluge.conftest import BaseTestCase +from deluge.core.core import Core +from deluge.core.rpcserver import RPCServer +from deluge.plugin_resource_manager import PluginResourceManager +from deluge.pluginmanagerbase import PluginManagerBase + +from . import common + +common.disable_new_release_check() + + +@mock.patch('deluge.core.pluginmanager.PluginManager', common.TestPluginManager) +class TestPluginResourceManager(BaseTestCase): + test_plugin_js = b'''/** + * Script: plugin_resources_test.js + * The client-side javascript code for the plugin_resources_test plugin. + * + * Copyright: + * (C) Gregorio Litenstein 2024 + * + * This file is part of plugin_resources_test and is licensed under GNU GPL 3.0, or + * later, with the additional special exception to link portions of this + * program with the OpenSSL library. See LICENSE for more details. + */ + +plugin_resources_testPlugin = Ext.extend(Deluge.Plugin, { + constructor: function(config) { + config = Ext.apply({ + name: 'plugin_resources_test' + }, config); + plugin_resources_testPlugin.superclass.constructor.call(this, config); + }, + + onDisable: function() { + deluge.preferences.removePage(this.prefsPage); + }, + + onEnable: function() { + this.prefsPage = deluge.preferences.addPage( + new Deluge.ux.preferences.plugin_resources_testPage()); + } +}); +new plugin_resources_testPlugin(); +''' + + def set_up(self): + self.rpcserver = RPCServer(listen=False) + self.core: Core = Core() + self.core.config.config['lsd'] = False + self.core.config.config['enabled_plugins'] = [] + self.listen_port = 51242 + return component.start() + + def tear_down(self): + def on_shutdown(result): + del self.rpcserver + del self.core + + return component.shutdown().addCallback(on_shutdown) + + async def test_resource_filename(self): + self.core.pluginmanager.scan_for_plugins() + await self.core.pluginmanager.enable_plugin('plugin_resources_test') + js_file_path = PluginResourceManager.resource_filename( + 'deluge_plugin_resources_test', 'data/plugin_resources_test.js' + ) + assert os.path.isfile(js_file_path) + with open(js_file_path, 'rb') as readfile: + contents = readfile.read() + assert contents == self.test_plugin_js + + async def test_reuse_previously_extracted_file(self): + def get_js_file(): + js_file = PluginResourceManager.resource_filename( + 'deluge_plugin_resources_test', 'data/plugin_resources_test.js' + ) + yield js_file + + self.core.pluginmanager.scan_for_plugins() + await self.core.pluginmanager.enable_plugin('plugin_resources_test') + js_file_path = '' + for i in range(5): + del i + if not js_file_path: + js_file_path = next(get_js_file()) + assert next(get_js_file()) == js_file_path and os.path.isfile(js_file_path) + + async def test_update_path_if_file_deleted(self): + self.core.pluginmanager.scan_for_plugins() + await self.core.pluginmanager.enable_plugin('plugin_resources_test') + js_file_path = PluginResourceManager.resource_filename( + 'deluge_plugin_resources_test', 'data/plugin_resources_test.js' + ) + assert os.path.isfile(js_file_path) + os.remove(js_file_path) + assert not os.path.isfile(js_file_path) + js_file_path_new = PluginResourceManager.resource_filename( + 'deluge_plugin_resources_test', 'data/plugin_resources_test.js' + ) + assert js_file_path != js_file_path_new and os.path.isfile(js_file_path_new) + + async def test_clean_files_on_disable_plugin(self): + self.core.pluginmanager.scan_for_plugins() + await self.core.pluginmanager.enable_plugin('plugin_resources_test') + js_file_path = PluginResourceManager.resource_filename( + 'deluge_plugin_resources_test', 'data/plugin_resources_test.js' + ) + assert os.path.isfile(js_file_path) + await self.core.pluginmanager.disable_plugin('plugin_resources_test') + assert not os.path.isfile(js_file_path) diff --git a/setup.cfg b/setup.cfg index 7bbfb30332..9b37bab143 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,9 @@ per-file-ignores = deluge/**/gtkui/*.py: E402 deluge/plugins/Stats/deluge_stats/graph.py: E402 deluge/plugin_resource_manager.py: N813 + deluge/tests/test_plugin_resources.py: F401, E501 + deluge/tests/test_plugin_metadata.py: E501 + deluge/tests/common.py: E501 setup.py : E402 # loop variable shares name with imported function deluge/common.py : F402