Skip to content

Commit 535b39d

Browse files
authored
✅ Add initial tests for python3 recipe (kivy#2192)
This covers an 84% of the python recipe. Most of the uncovered code is related with `set_lib_flags` (marked with a `todo` inline comment)
1 parent 0a76c4a commit 535b39d

File tree

2 files changed

+220
-2
lines changed

2 files changed

+220
-2
lines changed

pythonforandroid/recipes/python3/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
BuildInterruptingException,
1818
)
1919

20+
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE = (
21+
'Target ndk-api is {ndk_api}, '
22+
'but the python3 recipe supports only {min_ndk_api}+'
23+
)
24+
2025

2126
class Python3Recipe(TargetPythonRecipe):
2227
'''
@@ -270,8 +275,10 @@ def add_flags(include_flags, link_dirs, link_libs):
270275
def build_arch(self, arch):
271276
if self.ctx.ndk_api < self.MIN_NDK_API:
272277
raise BuildInterruptingException(
273-
'Target ndk-api is {}, but the python3 recipe supports only'
274-
' {}+'.format(self.ctx.ndk_api, self.MIN_NDK_API))
278+
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
279+
ndk_api=self.ctx.ndk_api, min_ndk_api=self.MIN_NDK_API
280+
),
281+
)
275282

276283
recipe_build_dir = self.get_build_dir(arch.arch)
277284

tests/recipes/test_python3.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import unittest
2+
3+
from os.path import join
4+
from unittest import mock
5+
6+
from pythonforandroid.recipes.python3 import (
7+
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE,
8+
)
9+
from pythonforandroid.util import BuildInterruptingException
10+
from tests.recipes.recipe_lib_test import RecipeCtx
11+
12+
13+
class TestPython3Recipe(RecipeCtx, unittest.TestCase):
14+
"""
15+
TestCase for recipe :mod:`~pythonforandroid.recipes.python3`
16+
"""
17+
recipe_name = "python3"
18+
19+
def test_property__libpython(self):
20+
self.assertEqual(
21+
self.recipe._libpython,
22+
f'libpython{self.recipe.major_minor_version_string}m.so'
23+
)
24+
25+
def test_should_build(self):
26+
expected_include_dir = join(
27+
self.recipe.get_build_dir(self.arch.arch), 'Include',
28+
)
29+
self.assertEqual(
30+
expected_include_dir, self.recipe.include_root(self.arch.arch)
31+
)
32+
33+
def test_include_root(self):
34+
expected_include_dir = join(
35+
self.recipe.get_build_dir(self.arch.arch), 'Include',
36+
)
37+
self.assertEqual(
38+
expected_include_dir, self.recipe.include_root(self.arch.arch)
39+
)
40+
41+
def test_link_root(self):
42+
expected_link_root = join(
43+
self.recipe.get_build_dir(self.arch.arch), 'android-build',
44+
)
45+
self.assertEqual(
46+
expected_link_root, self.recipe.link_root(self.arch.arch)
47+
)
48+
49+
@mock.patch("pythonforandroid.recipes.python3.subprocess.call")
50+
def test_compile_python_files(self, mock_subprocess):
51+
fake_compile_dir = '/fake/compile/dir'
52+
hostpy = self.recipe.ctx.hostpython = '/fake/hostpython3'
53+
self.recipe.compile_python_files(fake_compile_dir)
54+
mock_subprocess.assert_called_once_with(
55+
[hostpy, '-OO', '-m', 'compileall', '-b', '-f', fake_compile_dir],
56+
)
57+
58+
@mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices")
59+
@mock.patch("pythonforandroid.archs.glob")
60+
def test_get_recipe_env(
61+
self,
62+
mock_glob,
63+
mock_check_recipe_choices,
64+
):
65+
"""
66+
Test that method
67+
:meth:`~pythonforandroid.recipes.python3.Python3Recipe.get_recipe_env`
68+
returns the expected flags
69+
"""
70+
71+
mock_glob.return_value = ["llvm"]
72+
mock_check_recipe_choices.return_value = sorted(
73+
self.ctx.recipe_build_order
74+
)
75+
env = self.recipe.get_recipe_env(self.arch)
76+
77+
self.assertIn(
78+
f'-fPIC -DANDROID -D__ANDROID_API__={self.ctx.ndk_api}',
79+
env["CFLAGS"])
80+
self.assertEqual(env["CC"], self.arch.get_clang_exe(with_target=True))
81+
82+
# make sure that the mocked methods are actually called
83+
mock_glob.assert_called()
84+
mock_check_recipe_choices.assert_called()
85+
86+
def test_set_libs_flags(self):
87+
# todo: properly check `Python3Recipe.set_lib_flags`
88+
pass
89+
90+
# These decorators are to mock calls to `get_recipe_env`
91+
# and `set_libs_flags`, since these calls are tested separately
92+
@mock.patch("pythonforandroid.util.chdir")
93+
@mock.patch("pythonforandroid.util.makedirs")
94+
@mock.patch("pythonforandroid.archs.glob")
95+
def test_build_arch(
96+
self,
97+
mock_glob,
98+
mock_makedirs,
99+
mock_chdir,):
100+
mock_glob.return_value = ["llvm"]
101+
102+
# specific `build_arch` mocks
103+
with mock.patch(
104+
"builtins.open",
105+
mock.mock_open(read_data="#define ZLIB_VERSION 1.1\nfoo")
106+
) as mock_open_zlib, mock.patch(
107+
"pythonforandroid.recipes.python3.sh.Command"
108+
) as mock_sh_command, mock.patch(
109+
"pythonforandroid.recipes.python3.sh.make"
110+
) as mock_make, mock.patch(
111+
"pythonforandroid.recipes.python3.sh.cp"
112+
) as mock_cp:
113+
self.recipe.build_arch(self.arch)
114+
115+
# make sure that the mocked methods are actually called
116+
recipe_build_dir = self.recipe.get_build_dir(self.arch.arch)
117+
sh_command_calls = {
118+
f"{recipe_build_dir}/config.guess",
119+
f"{recipe_build_dir}/configure",
120+
}
121+
for command in sh_command_calls:
122+
self.assertIn(
123+
mock.call(command),
124+
mock_sh_command.mock_calls,
125+
)
126+
mock_open_zlib.assert_called()
127+
self.assertEqual(mock_make.call_count, 1)
128+
for make_call, kw in mock_make.call_args_list:
129+
self.assertIn(
130+
f'INSTSONAME={self.recipe._libpython}', make_call
131+
)
132+
mock_cp.assert_called_with(
133+
"pyconfig.h", join(recipe_build_dir, 'Include'),
134+
)
135+
mock_makedirs.assert_called()
136+
mock_chdir.assert_called()
137+
138+
def test_build_arch_wrong_ndk_api(self):
139+
# we check ndk_api using recipe's ctx
140+
self.recipe.ctx.ndk_api = 20
141+
with self.assertRaises(BuildInterruptingException) as e:
142+
self.recipe.build_arch(self.arch)
143+
self.assertEqual(
144+
e.exception.args[0],
145+
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
146+
ndk_api=self.recipe.ctx.ndk_api,
147+
min_ndk_api=self.recipe.MIN_NDK_API,
148+
),
149+
)
150+
# restore recipe's ctx or we could get failures with other test,
151+
# since we share `self.recipe with all the tests of the class
152+
self.recipe.ctx.ndk_api = self.ctx.ndk_api
153+
154+
@mock.patch('shutil.copystat')
155+
@mock.patch('shutil.copyfile')
156+
@mock.patch("pythonforandroid.util.chdir")
157+
@mock.patch("pythonforandroid.util.makedirs")
158+
@mock.patch("pythonforandroid.util.walk")
159+
@mock.patch("pythonforandroid.recipes.python3.sh.find")
160+
@mock.patch("pythonforandroid.recipes.python3.sh.cp")
161+
@mock.patch("pythonforandroid.recipes.python3.sh.zip")
162+
@mock.patch("pythonforandroid.recipes.python3.subprocess.call")
163+
def test_create_python_bundle(
164+
self,
165+
mock_subprocess,
166+
mock_sh_zip,
167+
mock_sh_cp,
168+
mock_sh_find,
169+
mock_walk,
170+
mock_makedirs,
171+
mock_chdir,
172+
mock_copyfile,
173+
mock_copystat,
174+
):
175+
fake_compile_dir = '/fake/compile/dir'
176+
simulated_walk_result = [
177+
["/fake_dir", ["__pycache__", "Lib"], ["README", "setup.py"]],
178+
["/fake_dir/Lib", ["ctypes"], ["abc.pyc", "abc.py"]],
179+
["/fake_dir/Lib/ctypes", [], ["util.pyc", "util.py"]],
180+
]
181+
mock_walk.return_value = simulated_walk_result
182+
self.recipe.create_python_bundle(fake_compile_dir, self.arch)
183+
184+
recipe_build_dir = self.recipe.get_build_dir(self.arch.arch)
185+
modules_build_dir = join(
186+
recipe_build_dir,
187+
'android-build',
188+
'build',
189+
'lib.linux{}-{}-{}'.format(
190+
'2' if self.recipe.version[0] == '2' else '',
191+
self.arch.command_prefix.split('-')[0],
192+
self.recipe.major_minor_version_string
193+
))
194+
expected_sp_paths = [
195+
modules_build_dir,
196+
join(recipe_build_dir, 'Lib'),
197+
self.ctx.get_python_install_dir(),
198+
]
199+
for n, (sp_call, kw) in enumerate(mock_subprocess.call_args_list):
200+
self.assertEqual(sp_call[0][-1], expected_sp_paths[n])
201+
202+
# we expect two calls to `walk_valid_filens`
203+
self.assertEqual(len(mock_walk.call_args_list), 2)
204+
205+
mock_sh_zip.assert_called()
206+
mock_sh_cp.assert_called()
207+
mock_sh_find.assert_called()
208+
mock_makedirs.assert_called()
209+
mock_chdir.assert_called()
210+
mock_copyfile.assert_called()
211+
mock_copystat.assert_called()

0 commit comments

Comments
 (0)