From 176a4e441cbc15e58923f2c4a82cab77892ae27f Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Tue, 23 Jul 2019 23:49:34 +0200 Subject: [PATCH] Basic toolchain.py unit tests First simple set of tests for toolchain.py Also refactors `Context.prepare_build_environment()` slightly. Splits concerns to improve readability and ease unit testing. --- pythonforandroid/build.py | 114 +++++++++++++++++++++++--------------- tests/test_toolchain.py | 62 +++++++++++++++++++++ 2 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 tests/test_toolchain.py diff --git a/pythonforandroid/build.py b/pythonforandroid/build.py index f0fbc86e9d..a5845358bf 100644 --- a/pythonforandroid/build.py +++ b/pythonforandroid/build.py @@ -27,6 +27,66 @@ RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API) +def get_cython_path(): + for cython_fn in ("cython", "cython3", "cython2", "cython-2.7"): + cython = sh.which(cython_fn) + if cython: + return cython + raise BuildInterruptingException('No cython binary found.') + + +def get_ndk_platform_dir(ndk_dir, ndk_api, arch): + ndk_platform_dir_exists = True + platform_dir = arch.platform_dir + ndk_platform = join( + ndk_dir, + 'platforms', + 'android-{}'.format(ndk_api), + platform_dir) + if not exists(ndk_platform): + warning("ndk_platform doesn't exist: {}".format(ndk_platform)) + ndk_platform_dir_exists = False + return ndk_platform, ndk_platform_dir_exists + + +def get_toolchain_versions(ndk_dir, arch): + toolchain_versions = [] + toolchain_path_exists = True + toolchain_prefix = arch.toolchain_prefix + toolchain_path = join(ndk_dir, 'toolchains') + if isdir(toolchain_path): + toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path, + toolchain_prefix)) + toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:] + for path in toolchain_contents] + else: + warning('Could not find toolchain subdirectory!') + toolchain_path_exists = False + return toolchain_versions, toolchain_path_exists + + +def get_targets(sdk_dir): + if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): + avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) + targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n') + elif exists(join(sdk_dir, 'tools', 'android')): + android = sh.Command(join(sdk_dir, 'tools', 'android')) + targets = android('list').stdout.decode('utf-8').split('\n') + else: + raise BuildInterruptingException( + 'Could not find `android` or `sdkmanager` binaries in Android SDK', + instructions='Make sure the path to the Android SDK is correct') + return targets + + +def get_available_apis(sdk_dir): + targets = get_targets(sdk_dir) + apis = [s for s in targets if re.match(r'^ *API level: ', s)] + apis = [re.findall(r'[0-9]+', s) for s in apis] + apis = [int(s[0]) for s in apis if s] + return apis + + class Context(object): '''A build context. If anything will be built, an instance this class will be instantiated and used to hold all the build state.''' @@ -238,20 +298,7 @@ def prepare_build_environment(self, self.android_api = android_api check_target_api(android_api, self.archs[0].arch) - - if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): - avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) - targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n') - elif exists(join(sdk_dir, 'tools', 'android')): - android = sh.Command(join(sdk_dir, 'tools', 'android')) - targets = android('list').stdout.decode('utf-8').split('\n') - else: - raise BuildInterruptingException( - 'Could not find `android` or `sdkmanager` binaries in Android SDK', - instructions='Make sure the path to the Android SDK is correct') - apis = [s for s in targets if re.match(r'^ *API level: ', s)] - apis = [re.findall(r'[0-9]+', s) for s in apis] - apis = [int(s[0]) for s in apis if s] + apis = get_available_apis(self.sdk_dir) info('Available Android APIs are ({})'.format( ', '.join(map(str, apis)))) if android_api in apis: @@ -327,46 +374,21 @@ def prepare_build_environment(self, if not self.ccache: info('ccache is missing, the build will not be optimized in the ' 'future.') - for cython_fn in ("cython", "cython3", "cython2", "cython-2.7"): - cython = sh.which(cython_fn) - if cython: - self.cython = cython - break - else: - raise BuildInterruptingException('No cython binary found.') - if not self.cython: - ok = False - warning("Missing requirement: cython is not installed") + self.cython = get_cython_path() # This would need to be changed if supporting multiarch APKs arch = self.archs[0] - platform_dir = arch.platform_dir toolchain_prefix = arch.toolchain_prefix - toolchain_version = None - self.ndk_platform = join( - self.ndk_dir, - 'platforms', - 'android-{}'.format(self.ndk_api), - platform_dir) - if not exists(self.ndk_platform): - warning('ndk_platform doesn\'t exist: {}'.format( - self.ndk_platform)) - ok = False + self.ndk_platform, ndk_platform_dir_exists = get_ndk_platform_dir( + self.ndk_dir, self.ndk_api, arch) + ok = ok and ndk_platform_dir_exists py_platform = sys.platform if py_platform in ['linux2', 'linux3']: py_platform = 'linux' - - toolchain_versions = [] - toolchain_path = join(self.ndk_dir, 'toolchains') - if isdir(toolchain_path): - toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path, - toolchain_prefix)) - toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:] - for path in toolchain_contents] - else: - warning('Could not find toolchain subdirectory!') - ok = False + toolchain_versions, toolchain_path_exists = get_toolchain_versions( + self.ndk_dir, arch) + ok = ok and toolchain_path_exists toolchain_versions.sort() toolchain_versions_gcc = [] diff --git a/tests/test_toolchain.py b/tests/test_toolchain.py new file mode 100644 index 0000000000..ce388c0c3b --- /dev/null +++ b/tests/test_toolchain.py @@ -0,0 +1,62 @@ +import sys +import pytest +import mock +from pythonforandroid.toolchain import ToolchainCL +from pythonforandroid.util import BuildInterruptingException + + +class TestToolchainCL: + + def test_help(self): + """ + Calling with `--help` should print help and exit 0. + """ + argv = ['toolchain.py', '--help', '--storage-dir=/tmp'] + with mock.patch('sys.argv', argv), pytest.raises(SystemExit) as ex_info, \ + mock.patch('argparse.ArgumentParser.print_help') as m_print_help: + ToolchainCL() + assert ex_info.value.code == 0 + assert m_print_help.call_args_list == [mock.call()] + + @pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3") + def test_unknown(self): + """ + Calling with unknown args should print help and exit 1. + """ + argv = ['toolchain.py', '--unknown'] + with mock.patch('sys.argv', argv), pytest.raises(SystemExit) as ex_info, \ + mock.patch('argparse.ArgumentParser.print_help') as m_print_help: + ToolchainCL() + assert ex_info.value.code == 1 + assert m_print_help.call_args_list == [mock.call()] + + def test_create(self): + """ + Basic `create` distribution test. + """ + argv = ['toolchain.py', 'create', '--sdk-dir=/tmp/android-sdk', '--ndk-dir=/tmp/android-ndk'] + with mock.patch('sys.argv', argv), \ + mock.patch('pythonforandroid.build.get_available_apis') as m_get_available_apis, \ + mock.patch('pythonforandroid.build.get_toolchain_versions') as m_get_toolchain_versions, \ + mock.patch('pythonforandroid.build.get_ndk_platform_dir') as m_get_ndk_platform_dir, \ + mock.patch('pythonforandroid.build.get_cython_path') as m_get_cython_path, \ + mock.patch('pythonforandroid.toolchain.build_dist_from_args') as m_build_dist_from_args: + m_get_available_apis.return_value = [27] + m_get_toolchain_versions.return_value = (['4.9'], True) + m_get_ndk_platform_dir.return_value = ( + '/tmp/android-ndk/platforms/android-21/arch-arm', True) + ToolchainCL() + assert m_get_available_apis.call_args_list == [mock.call('/tmp/android-sdk')] + assert m_get_toolchain_versions.call_args_list == [ + mock.call('/tmp/android-ndk', mock.ANY)] + assert m_get_cython_path.call_args_list == [mock.call()] + assert m_build_dist_from_args.call_count == 1 + + def test_create_no_sdk_dir(self): + """ + The `--sdk-dir` is mandatory to `create` a distribution. + """ + argv = ['toolchain.py', 'create'] + with mock.patch('sys.argv', argv), pytest.raises(BuildInterruptingException) as ex_info: + ToolchainCL() + assert ex_info.value.message == 'Android SDK dir was not specified, exiting.'