lbry-android-sdk/p4a/tests/test_recipe.py

315 lines
12 KiB
Python
Raw Normal View History

2022-11-29 21:35:24 +01:00
import os
import pytest
import types
import unittest
import warnings
from unittest import mock
from backports import tempfile
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(2**i) for i in range(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().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):
# avoid trying to find the recipe in a non-existing storage directory
self.ctx.storage_dir = None
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().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)
@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
):
"""
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/{self.ctx.ndk.host_tag}/bin/clang"
)
mock_find_executable.return_value = expected_compiler
arch = ArchAarch_64(self.ctx)
recipe = Recipe.get_recipe('libgeos', self.ctx)
assert recipe.need_stl_shared, True
env = recipe.get_recipe_env(arch)
# check that the mocks have been called
mock_ensure_dir.assert_called()
mock_find_executable.assert_called_once_with(
expected_compiler, path=self.ctx.env['PATH']
)
self.assertIsInstance(env, dict)
@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('libgeos', 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,
os.path.join(arch.ndk_lib_dir, f"lib{recipe.stl_lib_name}.so"),
)
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('libgeos', self.ctx)
assert recipe.need_stl_shared, True
recipe.postbuild_arch(arch)
mock_install_stl_lib.assert_called_once_with(arch)