Skip to content

Commit 312b522

Browse files
committed
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.
1 parent f6f6f19 commit 312b522

File tree

2 files changed

+157
-46
lines changed

2 files changed

+157
-46
lines changed

pythonforandroid/build.py

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,66 @@
2727
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
2828

2929

30+
def get_cython_path():
31+
for cython_fn in ("cython", "cython3", "cython2", "cython-2.7"):
32+
cython = sh.which(cython_fn)
33+
if cython:
34+
return cython
35+
raise BuildInterruptingException('No cython binary found.')
36+
37+
38+
def get_ndk_platform_dir(ndk_dir, ndk_api, arch):
39+
ndk_platform_dir_exists = True
40+
platform_dir = arch.platform_dir
41+
ndk_platform = join(
42+
ndk_dir,
43+
'platforms',
44+
'android-{}'.format(ndk_api),
45+
platform_dir)
46+
if not exists(ndk_platform):
47+
warning("ndk_platform doesn't exist: {}".format(ndk_platform))
48+
ndk_platform_dir_exists = False
49+
return ndk_platform, ndk_platform_dir_exists
50+
51+
52+
def get_toolchain_versions(ndk_dir, arch):
53+
toolchain_versions = []
54+
toolchain_path_exists = True
55+
toolchain_prefix = arch.toolchain_prefix
56+
toolchain_path = join(ndk_dir, 'toolchains')
57+
if isdir(toolchain_path):
58+
toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path,
59+
toolchain_prefix))
60+
toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:]
61+
for path in toolchain_contents]
62+
else:
63+
warning('Could not find toolchain subdirectory!')
64+
toolchain_path_exists = False
65+
return toolchain_versions, toolchain_path_exists
66+
67+
68+
def get_targets(sdk_dir):
69+
if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')):
70+
avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager'))
71+
targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n')
72+
elif exists(join(sdk_dir, 'tools', 'android')):
73+
android = sh.Command(join(sdk_dir, 'tools', 'android'))
74+
targets = android('list').stdout.decode('utf-8').split('\n')
75+
else:
76+
raise BuildInterruptingException(
77+
'Could not find `android` or `sdkmanager` binaries in Android SDK',
78+
instructions='Make sure the path to the Android SDK is correct')
79+
return targets
80+
81+
82+
def get_available_apis(sdk_dir):
83+
targets = get_targets(sdk_dir)
84+
apis = [s for s in targets if re.match(r'^ *API level: ', s)]
85+
apis = [re.findall(r'[0-9]+', s) for s in apis]
86+
apis = [int(s[0]) for s in apis if s]
87+
return apis
88+
89+
3090
class Context(object):
3191
'''A build context. If anything will be built, an instance this class
3292
will be instantiated and used to hold all the build state.'''
@@ -238,20 +298,7 @@ def prepare_build_environment(self,
238298
self.android_api = android_api
239299

240300
check_target_api(android_api, self.archs[0].arch)
241-
242-
if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')):
243-
avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager'))
244-
targets = avdmanager('list', 'target').stdout.decode('utf-8').split('\n')
245-
elif exists(join(sdk_dir, 'tools', 'android')):
246-
android = sh.Command(join(sdk_dir, 'tools', 'android'))
247-
targets = android('list').stdout.decode('utf-8').split('\n')
248-
else:
249-
raise BuildInterruptingException(
250-
'Could not find `android` or `sdkmanager` binaries in Android SDK',
251-
instructions='Make sure the path to the Android SDK is correct')
252-
apis = [s for s in targets if re.match(r'^ *API level: ', s)]
253-
apis = [re.findall(r'[0-9]+', s) for s in apis]
254-
apis = [int(s[0]) for s in apis if s]
301+
apis = get_available_apis(self.sdk_dir)
255302
info('Available Android APIs are ({})'.format(
256303
', '.join(map(str, apis))))
257304
if android_api in apis:
@@ -327,46 +374,21 @@ def prepare_build_environment(self,
327374
if not self.ccache:
328375
info('ccache is missing, the build will not be optimized in the '
329376
'future.')
330-
for cython_fn in ("cython", "cython3", "cython2", "cython-2.7"):
331-
cython = sh.which(cython_fn)
332-
if cython:
333-
self.cython = cython
334-
break
335-
else:
336-
raise BuildInterruptingException('No cython binary found.')
337-
if not self.cython:
338-
ok = False
339-
warning("Missing requirement: cython is not installed")
377+
self.cython = get_cython_path()
340378

341379
# This would need to be changed if supporting multiarch APKs
342380
arch = self.archs[0]
343-
platform_dir = arch.platform_dir
344381
toolchain_prefix = arch.toolchain_prefix
345-
toolchain_version = None
346-
self.ndk_platform = join(
347-
self.ndk_dir,
348-
'platforms',
349-
'android-{}'.format(self.ndk_api),
350-
platform_dir)
351-
if not exists(self.ndk_platform):
352-
warning('ndk_platform doesn\'t exist: {}'.format(
353-
self.ndk_platform))
354-
ok = False
382+
self.ndk_platform, ndk_platform_dir_exists = get_ndk_platform_dir(
383+
self.ndk_dir, self.ndk_api, arch)
384+
ok = ok and ndk_platform_dir_exists
355385

356386
py_platform = sys.platform
357387
if py_platform in ['linux2', 'linux3']:
358388
py_platform = 'linux'
359-
360-
toolchain_versions = []
361-
toolchain_path = join(self.ndk_dir, 'toolchains')
362-
if isdir(toolchain_path):
363-
toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path,
364-
toolchain_prefix))
365-
toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:]
366-
for path in toolchain_contents]
367-
else:
368-
warning('Could not find toolchain subdirectory!')
369-
ok = False
389+
toolchain_versions, toolchain_path_exists = get_toolchain_versions(
390+
self.ndk_dir, arch)
391+
ok = ok and toolchain_path_exists
370392
toolchain_versions.sort()
371393

372394
toolchain_versions_gcc = []

tests/test_toolchain.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import sys
2+
import pytest
3+
import mock
4+
from pythonforandroid.toolchain import ToolchainCL
5+
from pythonforandroid.util import BuildInterruptingException
6+
7+
8+
def patch_sys_argv(argv):
9+
return mock.patch('sys.argv', argv)
10+
11+
12+
def patch_argparse_print_help():
13+
return mock.patch('argparse.ArgumentParser.print_help')
14+
15+
16+
def raises_system_exit():
17+
return pytest.raises(SystemExit)
18+
19+
20+
class TestToolchainCL:
21+
22+
def test_help(self):
23+
"""
24+
Calling with `--help` should print help and exit 0.
25+
"""
26+
argv = ['toolchain.py', '--help', '--storage-dir=/tmp']
27+
with patch_sys_argv(argv), raises_system_exit(
28+
) as ex_info, patch_argparse_print_help() as m_print_help:
29+
ToolchainCL()
30+
assert ex_info.value.code == 0
31+
assert m_print_help.call_args_list == [mock.call()]
32+
33+
@pytest.mark.skipif(sys.version_info < (3, 0), reason="requires python3")
34+
def test_unknown(self):
35+
"""
36+
Calling with unknown args should print help and exit 1.
37+
"""
38+
argv = ['toolchain.py', '--unknown']
39+
with patch_sys_argv(argv), raises_system_exit(
40+
) as ex_info, patch_argparse_print_help() as m_print_help:
41+
ToolchainCL()
42+
assert ex_info.value.code == 1
43+
assert m_print_help.call_args_list == [mock.call()]
44+
45+
def test_create(self):
46+
"""
47+
Basic `create` distribution test.
48+
"""
49+
argv = [
50+
'toolchain.py',
51+
'create',
52+
'--sdk-dir=/tmp/android-sdk',
53+
'--ndk-dir=/tmp/android-ndk',
54+
'--dist-name=test_toolchain',
55+
]
56+
with patch_sys_argv(argv), mock.patch(
57+
'pythonforandroid.build.get_available_apis'
58+
) as m_get_available_apis, mock.patch(
59+
'pythonforandroid.build.get_toolchain_versions'
60+
) as m_get_toolchain_versions, mock.patch(
61+
'pythonforandroid.build.get_ndk_platform_dir'
62+
) as m_get_ndk_platform_dir, mock.patch(
63+
'pythonforandroid.build.get_cython_path'
64+
) as m_get_cython_path, mock.patch(
65+
'pythonforandroid.toolchain.build_dist_from_args'
66+
) as m_build_dist_from_args:
67+
m_get_available_apis.return_value = [27]
68+
m_get_toolchain_versions.return_value = (['4.9'], True)
69+
m_get_ndk_platform_dir.return_value = (
70+
'/tmp/android-ndk/platforms/android-21/arch-arm', True)
71+
ToolchainCL()
72+
assert m_get_available_apis.call_args_list == [
73+
mock.call('/tmp/android-sdk')]
74+
assert m_get_toolchain_versions.call_args_list == [
75+
mock.call('/tmp/android-ndk', mock.ANY)]
76+
assert m_get_cython_path.call_args_list == [mock.call()]
77+
assert m_build_dist_from_args.call_count == 1
78+
79+
def test_create_no_sdk_dir(self):
80+
"""
81+
The `--sdk-dir` is mandatory to `create` a distribution.
82+
"""
83+
argv = ['toolchain.py', 'create']
84+
with mock.patch('sys.argv', argv), pytest.raises(
85+
BuildInterruptingException
86+
) as ex_info:
87+
ToolchainCL()
88+
assert ex_info.value.message == (
89+
'Android SDK dir was not specified, exiting.')

0 commit comments

Comments
 (0)