forked from kivy/python-for-android
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_recipe.py
More file actions
357 lines (315 loc) · 13.2 KB
/
test_recipe.py
File metadata and controls
357 lines (315 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
import os
import pytest
import types
import unittest
import warnings
from unittest import mock
from backports import tempfile
from platform import system
from pythonforandroid.build import Context
from pythonforandroid.recipe import Recipe, import_recipe
from pythonforandroid.archs import ArchAarch_64
from pythonforandroid.bootstrap import Bootstrap
from test_bootstrap import BaseClassSetupBootstrap
def patch_logger(level):
return mock.patch('pythonforandroid.recipe.{}'.format(level))
def patch_logger_info():
return patch_logger('info')
def patch_logger_debug():
return patch_logger('debug')
def patch_urlretrieve():
return mock.patch('pythonforandroid.recipe.urlretrieve')
class DummyRecipe(Recipe):
pass
class TestRecipe(unittest.TestCase):
def test_recipe_dirs(self):
"""
Trivial `recipe_dirs()` test.
Makes sure the list is not empty and has the root directory.
"""
ctx = Context()
recipes_dir = Recipe.recipe_dirs(ctx)
# by default only the root dir `recipes` directory
self.assertEqual(len(recipes_dir), 1)
self.assertTrue(recipes_dir[0].startswith(ctx.root_dir))
def test_list_recipes(self):
"""
Trivial test verifying list_recipes returns a generator with some recipes.
"""
ctx = Context()
recipes = Recipe.list_recipes(ctx)
self.assertTrue(isinstance(recipes, types.GeneratorType))
recipes = list(recipes)
self.assertIn('python3', recipes)
def test_get_recipe(self):
"""
Makes sure `get_recipe()` returns a `Recipe` object when possible.
"""
ctx = Context()
recipe_name = 'python3'
recipe = Recipe.get_recipe(recipe_name, ctx)
self.assertTrue(isinstance(recipe, Recipe))
self.assertEqual(recipe.name, recipe_name)
recipe_name = 'does_not_exist'
with self.assertRaises(ValueError) as e:
Recipe.get_recipe(recipe_name, ctx)
self.assertEqual(
e.exception.args[0], 'Recipe does not exist: {}'.format(recipe_name))
def test_import_recipe(self):
"""
Verifies we can dynamically import a recipe without warnings.
"""
p4a_root_dir = os.path.dirname(os.path.dirname(__file__))
name = 'pythonforandroid.recipes.python3'
pathname = os.path.join(
*([p4a_root_dir] + name.split('.') + ['__init__.py'])
)
with warnings.catch_warnings(record=True) as recorded_warnings:
warnings.simplefilter("always")
module = import_recipe(name, pathname)
assert module is not None
assert recorded_warnings == []
def test_download_if_necessary(self):
"""
Download should happen via `Recipe.download()` only if the recipe
specific environment variable is not set.
"""
# download should happen as the environment variable is not set
recipe = DummyRecipe()
with mock.patch.object(Recipe, 'download') as m_download:
recipe.download_if_necessary()
assert m_download.call_args_list == [mock.call()]
# after setting it the download should be skipped
env_var = 'P4A_test_recipe_DIR'
env_dict = {env_var: '1'}
with mock.patch.object(Recipe, 'download') as m_download, mock.patch.dict(os.environ, env_dict):
recipe.download_if_necessary()
assert m_download.call_args_list == []
def test_download_url_not_set(self):
"""
Verifies that no download happens when URL is not set.
"""
recipe = DummyRecipe()
with patch_logger_info() as m_info:
recipe.download()
assert m_info.call_args_list == [
mock.call('Skipping test_recipe download as no URL is set')]
@staticmethod
def get_dummy_python_recipe_for_download_tests():
"""
Helper method for creating a test recipe used in download tests.
"""
recipe = DummyRecipe()
filename = 'Python-3.7.4.tgz'
url = 'https://www.python.org/ftp/python/3.7.4/{}'.format(filename)
recipe._url = url
recipe.ctx = Context()
return recipe, filename
def test_download_url_is_set(self):
"""
Verifies the actual download gets triggered when the URL is set.
"""
recipe, filename = self.get_dummy_python_recipe_for_download_tests()
url = recipe.url
with (
patch_logger_debug()) as m_debug, (
mock.patch.object(Recipe, 'download_file')) as m_download_file, (
mock.patch('pythonforandroid.recipe.sh.touch')) as m_touch, (
tempfile.TemporaryDirectory()) as temp_dir:
recipe.ctx.setup_dirs(temp_dir)
recipe.download()
assert m_download_file.call_args_list == [mock.call(url, filename)]
assert m_debug.call_args_list == [
mock.call(
'Downloading test_recipe from '
'https://www.python.org/ftp/python/3.7.4/Python-3.7.4.tgz')]
assert m_touch.call_count == 1
def test_download_file_scheme_https(self):
"""
Verifies `urlretrieve()` is being called on https downloads.
"""
recipe, filename = self.get_dummy_python_recipe_for_download_tests()
url = recipe.url
with (
patch_urlretrieve()) as m_urlretrieve, (
tempfile.TemporaryDirectory()) as temp_dir:
recipe.ctx.setup_dirs(temp_dir)
assert recipe.download_file(url, filename) == filename
assert m_urlretrieve.call_args_list == [
mock.call(url, filename, mock.ANY)
]
def test_download_file_scheme_https_oserror(self):
"""
Checks `urlretrieve()` is being retried on `OSError`.
After a number of retries the exception is re-reaised.
"""
recipe, filename = self.get_dummy_python_recipe_for_download_tests()
url = recipe.url
with (
patch_urlretrieve()) as m_urlretrieve, (
mock.patch('pythonforandroid.recipe.time.sleep')) as m_sleep, (
pytest.raises(OSError)), (
tempfile.TemporaryDirectory()) as temp_dir:
recipe.ctx.setup_dirs(temp_dir)
m_urlretrieve.side_effect = OSError
assert recipe.download_file(url, filename) == filename
retry = 5
expected_call_args_list = [mock.call(url, filename, mock.ANY)] * retry
assert m_urlretrieve.call_args_list == expected_call_args_list
expected_call_args_list = [mock.call(1)] * (retry - 1)
assert m_sleep.call_args_list == expected_call_args_list
class TestLibraryRecipe(BaseClassSetupBootstrap, unittest.TestCase):
def setUp(self):
"""
Initialize a Context with a Bootstrap and a Distribution to properly
test an library recipe, to do so we reuse `BaseClassSetupBootstrap`
"""
super(TestLibraryRecipe, self).setUp()
self.ctx.bootstrap = Bootstrap().get_bootstrap('sdl2', self.ctx)
self.setUp_distribution_with_bootstrap(self.ctx.bootstrap)
def test_built_libraries(self):
"""The openssl recipe is a library recipe, so it should have set the
attribute `built_libraries`, but not the case of `pyopenssl` recipe.
"""
recipe = Recipe.get_recipe('openssl', self.ctx)
self.assertTrue(recipe.built_libraries)
recipe = Recipe.get_recipe('pyopenssl', self.ctx)
self.assertFalse(recipe.built_libraries)
@mock.patch('pythonforandroid.recipe.exists')
def test_should_build(self, mock_exists):
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('openssl', self.ctx)
recipe.ctx = self.ctx
self.assertFalse(recipe.should_build(arch))
mock_exists.return_value = False
self.assertTrue(recipe.should_build(arch))
@mock.patch('pythonforandroid.recipe.Recipe.get_libraries')
@mock.patch('pythonforandroid.recipe.Recipe.install_libs')
def test_install_libraries(self, mock_install_libs, mock_get_libraries):
mock_get_libraries.return_value = {
'/build_lib/libsample1.so',
'/build_lib/libsample2.so',
}
self.ctx.recipe_build_order = [
"hostpython3",
"openssl",
"python3",
"sdl2",
"kivy",
]
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('openssl', self.ctx)
recipe.install_libraries(arch)
mock_install_libs.assert_called_once_with(
arch, *mock_get_libraries.return_value
)
class TesSTLRecipe(BaseClassSetupBootstrap, unittest.TestCase):
def setUp(self):
"""
Initialize a Context with a Bootstrap and a Distribution to properly
test a recipe which depends on android's STL library, to do so we reuse
`BaseClassSetupBootstrap`
"""
super(TesSTLRecipe, self).setUp()
self.ctx.bootstrap = Bootstrap().get_bootstrap('sdl2', self.ctx)
self.setUp_distribution_with_bootstrap(self.ctx.bootstrap)
self.ctx.python_recipe = Recipe.get_recipe('python3', self.ctx)
def test_get_stl_lib_dir(self):
"""
Test that :meth:`~pythonforandroid.recipe.STLRecipe.get_stl_lib_dir`
returns the expected path for the stl library
"""
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('icu', self.ctx)
self.assertTrue(recipe.need_stl_shared)
self.assertEqual(
recipe.get_stl_lib_dir(arch),
os.path.join(
self.ctx.ndk_dir,
'sources/cxx-stl/llvm-libc++/libs/{arch}'.format(
arch=arch.arch
),
),
)
@mock.patch("pythonforandroid.archs.glob")
@mock.patch('pythonforandroid.archs.find_executable')
@mock.patch('pythonforandroid.build.ensure_dir')
def test_get_recipe_env_with(
self, mock_ensure_dir, mock_find_executable, mock_glob
):
"""
Test that :meth:`~pythonforandroid.recipe.STLRecipe.get_recipe_env`
returns some expected keys and values.
.. note:: We don't check all the env variables, only those one specific
of :class:`~pythonforandroid.recipe.STLRecipe`, the others
should be tested in the proper test.
"""
expected_compiler = (
f"/opt/android/android-ndk/toolchains/"
f"llvm/prebuilt/{system().lower()}-x86_64/bin/clang"
)
mock_find_executable.return_value = expected_compiler
mock_glob.return_value = ["llvm"]
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('icu', self.ctx)
assert recipe.need_stl_shared, True
env = recipe.get_recipe_env(arch)
# check that the mocks have been called
mock_glob.assert_called()
mock_ensure_dir.assert_called()
mock_find_executable.assert_called_once_with(
expected_compiler, path=os.environ['PATH']
)
self.assertIsInstance(env, dict)
# check `CPPFLAGS`
expected_cppflags = {
'-I{stl_include}'.format(stl_include=recipe.stl_include_dir)
}
self.assertIn('CPPFLAGS', env)
for flags in expected_cppflags:
self.assertIn(flags, env['CPPFLAGS'])
# check `LIBS`
self.assertIn('LDFLAGS', env)
self.assertIn('-L' + recipe.get_stl_lib_dir(arch), env['LDFLAGS'])
self.assertIn('LIBS', env)
self.assertIn('-lc++_shared', env['LIBS'])
# check `CXXFLAGS` and `CXX`
for flag in {'CXXFLAGS', 'CXX'}:
self.assertIn(flag, env)
self.assertIn('-frtti -fexceptions', env[flag])
@mock.patch('pythonforandroid.recipe.Recipe.install_libs')
@mock.patch('pythonforandroid.recipe.isfile')
@mock.patch('pythonforandroid.build.ensure_dir')
def test_install_stl_lib(
self, mock_ensure_dir, mock_isfile, mock_install_lib
):
"""
Test that :meth:`~pythonforandroid.recipe.STLRecipe.install_stl_lib`,
calls the method :meth:`~pythonforandroid.recipe.Recipe.install_libs`
with the proper arguments: a subclass of
:class:`~pythonforandroid.archs.Arch` and our stl lib
(:attr:`~pythonforandroid.recipe.STLRecipe.stl_lib_name`)
"""
mock_isfile.return_value = False
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('icu', self.ctx)
recipe.ctx = self.ctx
assert recipe.need_stl_shared, True
recipe.install_stl_lib(arch)
mock_install_lib.assert_called_once_with(
arch,
'{ndk_dir}/sources/cxx-stl/llvm-libc++/'
'libs/{arch}/lib{stl_lib}.so'.format(
ndk_dir=self.ctx.ndk_dir,
arch=arch.arch,
stl_lib=recipe.stl_lib_name,
),
)
mock_ensure_dir.assert_called()
@mock.patch('pythonforandroid.recipe.Recipe.install_stl_lib')
def test_postarch_build(self, mock_install_stl_lib):
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('icu', self.ctx)
assert recipe.need_stl_shared, True
recipe.postbuild_arch(arch)
mock_install_stl_lib.assert_called_once_with(arch)