diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index 7ffc6b7e..70b35993 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # # fdroidserver/__main__.py - part of the FDroid server tools +# Copyright (C) 2020 Michael Pöhn # Copyright (C) 2010-2015, Ciaran Gultnieks, ciaran@ciarang.com # Copyright (C) 2013-2014 Daniel Marti # @@ -20,7 +21,9 @@ import sys import os import locale +import pkgutil import logging +import importlib import fdroidserver.common import fdroidserver.metadata @@ -54,25 +57,49 @@ commands = OrderedDict([ ]) -def print_help(): +def print_help(fdroid_modules=None): print(_("usage: ") + _("fdroid [] [-h|--help|--version|]")) print("") print(_("Valid commands are:")) for cmd, summary in commands.items(): print(" " + cmd + ' ' * (15 - len(cmd)) + summary) + if fdroid_modules: + print(_('commands from plugin modules:')) + for command in sorted(fdroid_modules.keys()): + print(' {:15}{}'.format(command, fdroid_modules[command]['summary'])) print("") +def find_plugins(): + fdroid_modules = [x[1] for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + commands = {} + for module_name in fdroid_modules: + try: + command_name = module_name[7:] + module = importlib.import_module(module_name) + if hasattr(module, 'fdroid_summary') and hasattr(module, 'main'): + commands[command_name] = {'summary': module.fdroid_summary, + 'module': module} + except IOError: + # We need to keep module lookup fault tolerant because buggy + # modules must not prevent fdroidserver from functioning + # TODO: think about warning users or debug logs for notifying devs + pass + return commands + + def main(): + sys.path.append(os.getcwd()) + fdroid_modules = find_plugins() if len(sys.argv) <= 1: - print_help() + print_help(fdroid_modules=fdroid_modules) sys.exit(0) command = sys.argv[1] - if command not in commands: + if command not in commands and command not in fdroid_modules.keys(): if command in ('-h', '--help'): - print_help() + print_help(fdroid_modules=fdroid_modules) sys.exit(0) elif command == '--version': output = _('no version info found!') @@ -103,7 +130,7 @@ def main(): sys.exit(0) else: print(_("Command '%s' not recognised.\n" % command)) - print_help() + print_help(fdroid_modules=fdroid_modules) sys.exit(1) verbose = any(s in sys.argv for s in ['-v', '--verbose']) @@ -133,7 +160,10 @@ def main(): sys.argv[0] += ' ' + command del sys.argv[1] - mod = __import__('fdroidserver.' + command, None, None, [command]) + if command in commands.keys(): + mod = __import__('fdroidserver.' + command, None, None, [command]) + else: + mod = fdroid_modules[command]['module'] system_langcode, system_encoding = locale.getdefaultlocale() if system_encoding is None or system_encoding.lower() not in ('utf-8', 'utf8'): diff --git a/tests/main.TestCase b/tests/main.TestCase index 8621cfcc..c303f181 100755 --- a/tests/main.TestCase +++ b/tests/main.TestCase @@ -4,8 +4,11 @@ import inspect import optparse import os import sys +import textwrap import unittest +import tempfile from unittest import mock +from testcommon import TmpCwd, TmpPyPath localmodule = os.path.realpath( os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')) @@ -17,7 +20,7 @@ from fdroidserver import common import fdroidserver.__main__ -class FdroidTest(unittest.TestCase): +class MainTest(unittest.TestCase): '''this tests fdroid.py''' def test_commands(self): @@ -49,18 +52,43 @@ class FdroidTest(unittest.TestCase): co = mock.Mock() with mock.patch('sys.argv', ['', 'init', '-h']): with mock.patch('fdroidserver.init.main', co): - with mock.patch('sys.exit', lambda x: None): + with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() + exit_mock.assert_called_once_with(0) co.assert_called_once_with() def test_call_deploy(self): co = mock.Mock() with mock.patch('sys.argv', ['', 'deploy', '-h']): with mock.patch('fdroidserver.server.main', co): - with mock.patch('sys.exit', lambda x: None): + with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() + exit_mock.assert_called_once_with(0) co.assert_called_once_with() + def test_find_plugins(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + main = lambda: 'all good'""")) + with TmpPyPath(tmpdir): + plugins = fdroidserver.__main__.find_plugins() + self.assertIn('testy', plugins.keys()) + self.assertEqual(plugins['testy']['summary'], 'ttt') + self.assertEqual(plugins['testy']['module'].main(), 'all good') + + def test_main_plugin(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + main = lambda: pritn('all good')""")) + with mock.patch('sys.argv', ['', 'testy']): + with mock.patch('sys.exit') as exit_mock: + fdroidserver.__main__.main() + exit_mock.assert_called_once_with(0) + if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) @@ -71,5 +99,5 @@ if __name__ == "__main__": (common.options, args) = parser.parse_args(['--verbose']) newSuite = unittest.TestSuite() - newSuite.addTest(unittest.makeSuite(FdroidTest)) + newSuite.addTest(unittest.makeSuite(MainTest)) unittest.main(failfast=False) diff --git a/tests/testcommon.py b/tests/testcommon.py index a637012e..2557bd61 100644 --- a/tests/testcommon.py +++ b/tests/testcommon.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import os +import sys class TmpCwd(): @@ -32,3 +33,18 @@ class TmpCwd(): def __exit__(self, a, b, c): os.chdir(self.orig_cwd) + + +class TmpPyPath(): + """Context-manager for temporarily changing the current working + directory. + """ + + def __init__(self, additional_path): + self.additional_path = additional_path + + def __enter__(self): + sys.path.append(self.additional_path) + + def __exit__(self, a, b, c): + sys.path.remove(self.additional_path)