From bf815251ec04a74f24424d2f8b28f9089305668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Thu, 23 Jan 2020 04:13:14 +0100 Subject: [PATCH 1/5] rough plugin system implementation --- fdroidserver/__main__.py | 42 ++++++++++++++++++++++++++++++++++------ tests/main.TestCase | 36 ++++++++++++++++++++++++++++++---- tests/testcommon.py | 16 +++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) 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) From 3a3803ea2deba72257f29ec654917bb271f720f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 29 Jan 2020 12:39:21 +0100 Subject: [PATCH 2/5] raise excepten when starting broken plugin --- fdroidserver/__main__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index 70b35993..fa7eb02a 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -74,17 +74,19 @@ 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: + command_name = module_name[7:] 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: + except Exception as e: # 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 + if len(sys.argv) > 1 and sys.argv[1] == command_name: + # only raise exeption when a user specifies the broken + # plugin in explicitly in command line + raise e return commands From b257a3411a9dec1a22e53b46eef0827a3c202a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 29 Jan 2020 13:08:43 +0100 Subject: [PATCH 3/5] stick to default python sys.path --- fdroidserver/__main__.py | 1 - tests/main.TestCase | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index fa7eb02a..46cf87df 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -91,7 +91,6 @@ def find_plugins(): def main(): - sys.path.append(os.getcwd()) fdroid_modules = find_plugins() if len(sys.argv) <= 1: diff --git a/tests/main.TestCase b/tests/main.TestCase index c303f181..e33c6758 100755 --- a/tests/main.TestCase +++ b/tests/main.TestCase @@ -84,10 +84,13 @@ class MainTest(unittest.TestCase): 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) + test_path = sys.path.copy() + test_path.append(tmpdir) + with mock.patch('sys.path', test_path): + 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__": From 77167e098ece2a8b93d8f9bcb8c52737ff1750d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 10 Jun 2020 18:43:11 +0200 Subject: [PATCH 4/5] plugin system: regex instead of import bases plugin parsing --- fdroidserver/__main__.py | 53 ++++++++++++++++++++----- tests/main.TestCase | 85 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 122 insertions(+), 16 deletions(-) diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index 46cf87df..785179de 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -18,12 +18,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import re import sys import os import locale import pkgutil import logging -import importlib import fdroidserver.common import fdroidserver.metadata @@ -70,16 +70,51 @@ def print_help(fdroid_modules=None): print("") +def preparse_plugin(module_name, module_dir): + """simple regex based parsing for plugin scripts, + so we don't have to import them when we just need the summary, + but not plan on executing this particular plugin.""" + if '.' in module_name: + raise ValueError("No '.' allowed in fdroid plugin modules: '{}'" + .format(module_name)) + path = os.path.join(module_dir, module_name + '.py') + if not os.path.isfile(path): + path = os.path.join(module_dir, module_name, '__main__.py') + if not os.path.isfile(path): + raise ValueError("unable to find main plugin script " + "for module '{n}' ('{d}')" + .format(n=module_name, + d=module_dir)) + summary = None + main = None + with open(path, 'r', encoding='utf-8') as f: + re_main = re.compile(r'^(\s*def\s+main\s*\(.*\)\s*:' + r'|\s*main\s*=\s*lambda\s*:.+)$') + re_summary = re.compile(r'^\s*fdroid_summary\s*=\s["\'](?P.+)["\']$') + for line in f: + m_summary = re_summary.match(line) + if m_summary: + summary = m_summary.group('text') + if re_main.match(line): + main = True + + if summary is None: + raise NameError("could not find 'fdroid_summary' in: '{}' plugin" + .format(module_name)) + if main is None: + raise NameError("could not find 'main' function in: '{}' plugin" + .format(module_name)) + return {'name': module_name, 'summary': summary} + + def find_plugins(): - fdroid_modules = [x[1] for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + found_plugins = [{'name': x[1], 'dir': x[0].path} for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] commands = {} - for module_name in fdroid_modules: - command_name = module_name[7:] + for plugin_def in found_plugins: + command_name = plugin_def['name'][7:] try: - module = importlib.import_module(module_name) - if hasattr(module, 'fdroid_summary') and hasattr(module, 'main'): - commands[command_name] = {'summary': module.fdroid_summary, - 'module': module} + commands[command_name] = preparse_plugin(plugin_def['name'], + plugin_def['dir']) except Exception as e: # We need to keep module lookup fault tolerant because buggy # modules must not prevent fdroidserver from functioning @@ -164,7 +199,7 @@ def main(): if command in commands.keys(): mod = __import__('fdroidserver.' + command, None, None, [command]) else: - mod = fdroid_modules[command]['module'] + mod = __import__(fdroid_modules[command]['name'], None, None, [command]) 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 e33c6758..35b80bc2 100755 --- a/tests/main.TestCase +++ b/tests/main.TestCase @@ -4,6 +4,7 @@ import inspect import optparse import os import sys +import pkgutil import textwrap import unittest import tempfile @@ -74,24 +75,94 @@ class MainTest(unittest.TestCase): 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') + self.assertIn('testy', plugins.keys()) + self.assertEqual(plugins['testy']['summary'], 'ttt') + self.assertEqual(__import__(plugins['testy']['name'], + None, + None, + ['testy']) + .main(), + 'all good') - def test_main_plugin(self): + def test_main_plugin_lambda(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')""")) - test_path = sys.path.copy() - test_path.append(tmpdir) - with mock.patch('sys.path', test_path): + with TmpPyPath(tmpdir): with mock.patch('sys.argv', ['', 'testy']): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() exit_mock.assert_called_once_with(0) + def test_main_plugin_def(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + def main(): + pritn('all good')""")) + with TmpPyPath(tmpdir): + with mock.patch('sys.argv', ['', 'testy']): + with mock.patch('sys.exit') as exit_mock: + fdroidserver.__main__.main() + exit_mock.assert_called_once_with(0) + + def test_preparse_plugin_lookup_bad_name(self): + self.assertRaises(ValueError, + fdroidserver.__main__.preparse_plugin, + "some.package", "/non/existent/module/path") + + def test_preparse_plugin_lookup_bad_path(self): + self.assertRaises(ValueError, + fdroidserver.__main__.preparse_plugin, + "fake_module_name", "/non/existent/module/path") + + def test_preparse_plugin_lookup_summary_missing(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy.py', 'w') as f: + f.write(textwrap.dedent("""\ + main = lambda: print('all good')""")) + with TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_dir = p[0][0].path + module_name = p[0][1] + self.assertRaises(NameError, + fdroidserver.__main__.preparse_plugin, + module_name, module_dir) + + def test_preparse_plugin_lookup_module_file(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 TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_path = p[0][0].path + module_name = p[0][1] + d = fdroidserver.__main__.preparse_plugin(module_name, module_path) + self.assertDictEqual(d, {'name': 'fdroid_testy', + 'summary': 'ttt'}) + + def test_preparse_plugin_lookup_module_dir(self): + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + os.mkdir(os.path.join(tmpdir, 'fdroid_testy')) + with open('fdroid_testy/__main__.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + main = lambda: print('all good')""")) + with open('fdroid_testy/__init__.py', 'w') as f: + pass + with TmpPyPath(tmpdir): + p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] + module_path = p[0][0].path + module_name = p[0][1] + d = fdroidserver.__main__.preparse_plugin(module_name, module_path) + self.assertDictEqual(d, {'name': 'fdroid_testy', + 'summary': 'ttt'}) + if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) From a97b3ca4dd30999091c8afc4e9ddd4d354ae09ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20P=C3=B6hn?= Date: Wed, 10 Jun 2020 18:15:38 +0200 Subject: [PATCH 5/5] implement plugin system review suggestsions --- fdroidserver/__main__.py | 36 ++++++++-------- tests/main.TestCase | 93 +++++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 38 deletions(-) diff --git a/fdroidserver/__main__.py b/fdroidserver/__main__.py index 785179de..770d8c13 100755 --- a/fdroidserver/__main__.py +++ b/fdroidserver/__main__.py @@ -32,7 +32,7 @@ from argparse import ArgumentError from collections import OrderedDict -commands = OrderedDict([ +COMMANDS = OrderedDict([ ("build", _("Build a package from source")), ("init", _("Quickly start a new repository")), ("publish", _("Sign and place packages in the repo")), @@ -57,16 +57,16 @@ commands = OrderedDict([ ]) -def print_help(fdroid_modules=None): +def print_help(available_plugins=None): print(_("usage: ") + _("fdroid [] [-h|--help|--version|]")) print("") print(_("Valid commands are:")) - for cmd, summary in commands.items(): + for cmd, summary in COMMANDS.items(): print(" " + cmd + ' ' * (15 - len(cmd)) + summary) - if fdroid_modules: + if available_plugins: print(_('commands from plugin modules:')) - for command in sorted(fdroid_modules.keys()): - print(' {:15}{}'.format(command, fdroid_modules[command]['summary'])) + for command in sorted(available_plugins.keys()): + print(' {:15}{}'.format(command, available_plugins[command]['summary'])) print("") @@ -109,12 +109,12 @@ def preparse_plugin(module_name, module_dir): def find_plugins(): found_plugins = [{'name': x[1], 'dir': x[0].path} for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] - commands = {} + plugin_infos = {} for plugin_def in found_plugins: command_name = plugin_def['name'][7:] try: - commands[command_name] = preparse_plugin(plugin_def['name'], - plugin_def['dir']) + plugin_infos[command_name] = preparse_plugin(plugin_def['name'], + plugin_def['dir']) except Exception as e: # We need to keep module lookup fault tolerant because buggy # modules must not prevent fdroidserver from functioning @@ -122,20 +122,20 @@ def find_plugins(): # only raise exeption when a user specifies the broken # plugin in explicitly in command line raise e - return commands + return plugin_infos def main(): - fdroid_modules = find_plugins() + available_plugins = find_plugins() if len(sys.argv) <= 1: - print_help(fdroid_modules=fdroid_modules) + print_help(available_plugins=available_plugins) sys.exit(0) command = sys.argv[1] - if command not in commands and command not in fdroid_modules.keys(): + if command not in COMMANDS and command not in available_plugins.keys(): if command in ('-h', '--help'): - print_help(fdroid_modules=fdroid_modules) + print_help(available_plugins=available_plugins) sys.exit(0) elif command == '--version': output = _('no version info found!') @@ -162,11 +162,11 @@ def main(): else: from pkg_resources import get_distribution output = get_distribution('fdroidserver').version + '\n' - print(output), + print(output) sys.exit(0) else: print(_("Command '%s' not recognised.\n" % command)) - print_help(fdroid_modules=fdroid_modules) + print_help(available_plugins=available_plugins) sys.exit(1) verbose = any(s in sys.argv for s in ['-v', '--verbose']) @@ -196,10 +196,10 @@ def main(): sys.argv[0] += ' ' + command del sys.argv[1] - if command in commands.keys(): + if command in COMMANDS.keys(): mod = __import__('fdroidserver.' + command, None, None, [command]) else: - mod = __import__(fdroid_modules[command]['name'], None, None, [command]) + mod = __import__(available_plugins[command]['name'], None, None, [command]) 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 35b80bc2..e05efd95 100755 --- a/tests/main.TestCase +++ b/tests/main.TestCase @@ -24,9 +24,9 @@ import fdroidserver.__main__ class MainTest(unittest.TestCase): '''this tests fdroid.py''' - def test_commands(self): + def test_COMMANDS_check(self): """make sure the built in sub-command defs didn't change unintentionally""" - self.assertListEqual([x for x in fdroidserver.__main__.commands.keys()], + self.assertListEqual([x for x in fdroidserver.__main__.COMMANDS.keys()], ['build', 'init', 'publish', @@ -55,6 +55,8 @@ class MainTest(unittest.TestCase): with mock.patch('fdroidserver.init.main', co): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() + # note: this is sloppy, if `init` changes + # this might need changing too exit_mock.assert_called_once_with(0) co.assert_called_once_with() @@ -64,51 +66,102 @@ class MainTest(unittest.TestCase): with mock.patch('fdroidserver.server.main', co): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() + # note: this is sloppy, if `deploy` changes + # this might need changing too 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: + with open('fdroid_testy1.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(__import__(plugins['testy']['name'], + self.assertIn('testy1', plugins.keys()) + self.assertEqual(plugins['testy1']['summary'], 'ttt') + self.assertEqual(__import__(plugins['testy1']['name'], None, None, - ['testy']) + ['testy1']) .main(), 'all good') def test_main_plugin_lambda(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): - with open('fdroid_testy.py', 'w') as f: + with open('fdroid_testy2.py', 'w') as f: f.write(textwrap.dedent("""\ fdroid_summary = "ttt" - main = lambda: pritn('all good')""")) + main = lambda: print('all good')""")) with TmpPyPath(tmpdir): - with mock.patch('sys.argv', ['', 'testy']): + with mock.patch('sys.argv', ['', 'testy2']): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() exit_mock.assert_called_once_with(0) def test_main_plugin_def(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): - with open('fdroid_testy.py', 'w') as f: + with open('fdroid_testy3.py', 'w') as f: f.write(textwrap.dedent("""\ fdroid_summary = "ttt" def main(): - pritn('all good')""")) + print('all good')""")) with TmpPyPath(tmpdir): - with mock.patch('sys.argv', ['', 'testy']): + with mock.patch('sys.argv', ['', 'testy3']): with mock.patch('sys.exit') as exit_mock: fdroidserver.__main__.main() exit_mock.assert_called_once_with(0) + def test_main_broken_plugin(self): + """making sure broken plugins get their exceptions through""" + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy4.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + def main(): + raise Exception("this plugin is broken")""")) + with TmpPyPath(tmpdir): + with mock.patch('sys.argv', ['', 'testy4']): + with self.assertRaisesRegex(Exception, "this plugin is broken"): + fdroidserver.__main__.main() + + def test_main_malicious_plugin(self): + """The purpose of this test is to make sure code in plugins + doesn't get executed unintentionally. + """ + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_testy5.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + raise Exception("this plugin is malicious") + def main(): + print("evil things")""")) + with TmpPyPath(tmpdir): + with mock.patch('sys.argv', ['', 'lint']): + with mock.patch('sys.exit') as exit_mock: + fdroidserver.__main__.main() + # note: this is sloppy, if `lint` changes + # this might need changing too + exit_mock.assert_called_once_with(0) + + def test_main_prevent_plugin_override(self): + """making sure build-in subcommands cannot be overridden by plugins + """ + with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): + with open('fdroid_signatures.py', 'w') as f: + f.write(textwrap.dedent("""\ + fdroid_summary = "ttt" + def main(): + raise("plugin overrides don't get prevent!")""")) + with TmpPyPath(tmpdir): + with mock.patch('sys.argv', ['', 'signatures']): + with mock.patch('sys.exit') as exit_mock: + fdroidserver.__main__.main() + # note: this is sloppy, if `signatures` changes + # this might need changing too + self.assertEqual(exit_mock.call_count, 2) + def test_preparse_plugin_lookup_bad_name(self): self.assertRaises(ValueError, fdroidserver.__main__.preparse_plugin, @@ -121,7 +174,7 @@ class MainTest(unittest.TestCase): def test_preparse_plugin_lookup_summary_missing(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): - with open('fdroid_testy.py', 'w') as f: + with open('fdroid_testy6.py', 'w') as f: f.write(textwrap.dedent("""\ main = lambda: print('all good')""")) with TmpPyPath(tmpdir): @@ -134,7 +187,7 @@ class MainTest(unittest.TestCase): def test_preparse_plugin_lookup_module_file(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): - with open('fdroid_testy.py', 'w') as f: + with open('fdroid_testy7.py', 'w') as f: f.write(textwrap.dedent("""\ fdroid_summary = "ttt" main = lambda: pritn('all good')""")) @@ -143,24 +196,24 @@ class MainTest(unittest.TestCase): module_path = p[0][0].path module_name = p[0][1] d = fdroidserver.__main__.preparse_plugin(module_name, module_path) - self.assertDictEqual(d, {'name': 'fdroid_testy', + self.assertDictEqual(d, {'name': 'fdroid_testy7', 'summary': 'ttt'}) def test_preparse_plugin_lookup_module_dir(self): with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir): - os.mkdir(os.path.join(tmpdir, 'fdroid_testy')) - with open('fdroid_testy/__main__.py', 'w') as f: + os.mkdir(os.path.join(tmpdir, 'fdroid_testy8')) + with open('fdroid_testy8/__main__.py', 'w') as f: f.write(textwrap.dedent("""\ fdroid_summary = "ttt" main = lambda: print('all good')""")) - with open('fdroid_testy/__init__.py', 'w') as f: + with open('fdroid_testy8/__init__.py', 'w') as f: pass with TmpPyPath(tmpdir): p = [x for x in pkgutil.iter_modules() if x[1].startswith('fdroid_')] module_path = p[0][0].path module_name = p[0][1] d = fdroidserver.__main__.preparse_plugin(module_name, module_path) - self.assertDictEqual(d, {'name': 'fdroid_testy', + self.assertDictEqual(d, {'name': 'fdroid_testy8', 'summary': 'ttt'})