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)