212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
|
import unittest
|
||
|
|
||
|
from os.path import join
|
||
|
from unittest import mock
|
||
|
|
||
|
from pythonforandroid.recipes.python3 import (
|
||
|
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE,
|
||
|
)
|
||
|
from pythonforandroid.util import BuildInterruptingException, build_platform
|
||
|
from tests.recipes.recipe_lib_test import RecipeCtx
|
||
|
|
||
|
|
||
|
class TestPython3Recipe(RecipeCtx, unittest.TestCase):
|
||
|
"""
|
||
|
TestCase for recipe :mod:`~pythonforandroid.recipes.python3`
|
||
|
"""
|
||
|
recipe_name = "python3"
|
||
|
expected_compiler = (
|
||
|
f"/opt/android/android-ndk/toolchains/"
|
||
|
f"llvm/prebuilt/{build_platform}/bin/clang"
|
||
|
)
|
||
|
|
||
|
def test_property__libpython(self):
|
||
|
self.assertEqual(
|
||
|
self.recipe._libpython,
|
||
|
f'libpython{self.recipe.link_version}.so'
|
||
|
)
|
||
|
|
||
|
@mock.patch('pythonforandroid.recipes.python3.Path.is_file')
|
||
|
def test_should_build(self, mock_is_file):
|
||
|
# in case that python lib exists, we shouldn't trigger the build
|
||
|
self.assertFalse(self.recipe.should_build(self.arch))
|
||
|
# in case that python lib doesn't exist, we should trigger the build
|
||
|
mock_is_file.return_value = False
|
||
|
self.assertTrue(self.recipe.should_build(self.arch))
|
||
|
|
||
|
def test_include_root(self):
|
||
|
expected_include_dir = join(
|
||
|
self.recipe.get_build_dir(self.arch.arch), 'Include',
|
||
|
)
|
||
|
self.assertEqual(
|
||
|
expected_include_dir, self.recipe.include_root(self.arch.arch)
|
||
|
)
|
||
|
|
||
|
def test_link_root(self):
|
||
|
expected_link_root = join(
|
||
|
self.recipe.get_build_dir(self.arch.arch), 'android-build',
|
||
|
)
|
||
|
self.assertEqual(
|
||
|
expected_link_root, self.recipe.link_root(self.arch.arch)
|
||
|
)
|
||
|
|
||
|
@mock.patch("pythonforandroid.recipes.python3.subprocess.call")
|
||
|
def test_compile_python_files(self, mock_subprocess):
|
||
|
fake_compile_dir = '/fake/compile/dir'
|
||
|
hostpy = self.recipe.ctx.hostpython = '/fake/hostpython3'
|
||
|
self.recipe.compile_python_files(fake_compile_dir)
|
||
|
mock_subprocess.assert_called_once_with(
|
||
|
[hostpy, '-OO', '-m', 'compileall', '-b', '-f', fake_compile_dir],
|
||
|
)
|
||
|
|
||
|
@mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices")
|
||
|
@mock.patch("pythonforandroid.archs.find_executable")
|
||
|
def test_get_recipe_env(
|
||
|
self,
|
||
|
mock_find_executable,
|
||
|
mock_check_recipe_choices,
|
||
|
):
|
||
|
"""
|
||
|
Test that method
|
||
|
:meth:`~pythonforandroid.recipes.python3.Python3Recipe.get_recipe_env`
|
||
|
returns the expected flags
|
||
|
"""
|
||
|
mock_find_executable.return_value = self.expected_compiler
|
||
|
mock_check_recipe_choices.return_value = sorted(
|
||
|
self.ctx.recipe_build_order
|
||
|
)
|
||
|
env = self.recipe.get_recipe_env(self.arch)
|
||
|
|
||
|
self.assertIn('-fPIC -DANDROID', env["CFLAGS"])
|
||
|
self.assertEqual(env["CC"], self.arch.get_clang_exe(with_target=True))
|
||
|
|
||
|
# make sure that the mocked methods are actually called
|
||
|
mock_check_recipe_choices.assert_called()
|
||
|
|
||
|
def test_set_libs_flags(self):
|
||
|
# todo: properly check `Python3Recipe.set_lib_flags`
|
||
|
pass
|
||
|
|
||
|
# These decorators are to mock calls to `get_recipe_env`
|
||
|
# and `set_libs_flags`, since these calls are tested separately
|
||
|
@mock.patch("pythonforandroid.util.chdir")
|
||
|
@mock.patch("pythonforandroid.util.makedirs")
|
||
|
@mock.patch("pythonforandroid.archs.find_executable")
|
||
|
def test_build_arch(
|
||
|
self,
|
||
|
mock_find_executable,
|
||
|
mock_makedirs,
|
||
|
mock_chdir):
|
||
|
mock_find_executable.return_value = self.expected_compiler
|
||
|
|
||
|
# specific `build_arch` mocks
|
||
|
with mock.patch(
|
||
|
"builtins.open",
|
||
|
mock.mock_open(read_data="#define ZLIB_VERSION 1.1\nfoo")
|
||
|
) as mock_open_zlib, mock.patch(
|
||
|
"pythonforandroid.recipes.python3.sh.Command"
|
||
|
) as mock_sh_command, mock.patch(
|
||
|
"pythonforandroid.recipes.python3.sh.make"
|
||
|
) as mock_make, mock.patch(
|
||
|
"pythonforandroid.recipes.python3.sh.cp"
|
||
|
) as mock_cp:
|
||
|
self.recipe.build_arch(self.arch)
|
||
|
|
||
|
# make sure that the mocked methods are actually called
|
||
|
recipe_build_dir = self.recipe.get_build_dir(self.arch.arch)
|
||
|
sh_command_calls = {
|
||
|
f"{recipe_build_dir}/config.guess",
|
||
|
f"{recipe_build_dir}/configure",
|
||
|
}
|
||
|
for command in sh_command_calls:
|
||
|
self.assertIn(
|
||
|
mock.call(command),
|
||
|
mock_sh_command.mock_calls,
|
||
|
)
|
||
|
mock_open_zlib.assert_called()
|
||
|
self.assertEqual(mock_make.call_count, 1)
|
||
|
for make_call, kw in mock_make.call_args_list:
|
||
|
self.assertIn(
|
||
|
f'INSTSONAME={self.recipe._libpython}', make_call
|
||
|
)
|
||
|
mock_cp.assert_called_with(
|
||
|
"pyconfig.h", join(recipe_build_dir, 'Include'),
|
||
|
)
|
||
|
mock_makedirs.assert_called()
|
||
|
mock_chdir.assert_called()
|
||
|
|
||
|
def test_build_arch_wrong_ndk_api(self):
|
||
|
# we check ndk_api using recipe's ctx
|
||
|
self.recipe.ctx.ndk_api = 20
|
||
|
with self.assertRaises(BuildInterruptingException) as e:
|
||
|
self.recipe.build_arch(self.arch)
|
||
|
self.assertEqual(
|
||
|
e.exception.args[0],
|
||
|
NDK_API_LOWER_THAN_SUPPORTED_MESSAGE.format(
|
||
|
ndk_api=self.recipe.ctx.ndk_api,
|
||
|
min_ndk_api=self.recipe.MIN_NDK_API,
|
||
|
),
|
||
|
)
|
||
|
# restore recipe's ctx or we could get failures with other test,
|
||
|
# since we share `self.recipe with all the tests of the class
|
||
|
self.recipe.ctx.ndk_api = self.ctx.ndk_api
|
||
|
|
||
|
@mock.patch('shutil.copystat')
|
||
|
@mock.patch('shutil.copyfile')
|
||
|
@mock.patch("pythonforandroid.util.chdir")
|
||
|
@mock.patch("pythonforandroid.util.makedirs")
|
||
|
@mock.patch("pythonforandroid.util.walk")
|
||
|
@mock.patch("pythonforandroid.recipes.python3.sh.find")
|
||
|
@mock.patch("pythonforandroid.recipes.python3.sh.cp")
|
||
|
@mock.patch("pythonforandroid.recipes.python3.sh.zip")
|
||
|
@mock.patch("pythonforandroid.recipes.python3.subprocess.call")
|
||
|
def test_create_python_bundle(
|
||
|
self,
|
||
|
mock_subprocess,
|
||
|
mock_sh_zip,
|
||
|
mock_sh_cp,
|
||
|
mock_sh_find,
|
||
|
mock_walk,
|
||
|
mock_makedirs,
|
||
|
mock_chdir,
|
||
|
mock_copyfile,
|
||
|
mock_copystat,
|
||
|
):
|
||
|
fake_compile_dir = '/fake/compile/dir'
|
||
|
simulated_walk_result = [
|
||
|
["/fake_dir", ["__pycache__", "Lib"], ["README", "setup.py"]],
|
||
|
["/fake_dir/Lib", ["ctypes"], ["abc.pyc", "abc.py"]],
|
||
|
["/fake_dir/Lib/ctypes", [], ["util.pyc", "util.py"]],
|
||
|
]
|
||
|
mock_walk.return_value = simulated_walk_result
|
||
|
self.recipe.create_python_bundle(fake_compile_dir, self.arch)
|
||
|
|
||
|
recipe_build_dir = self.recipe.get_build_dir(self.arch.arch)
|
||
|
modules_build_dir = join(
|
||
|
recipe_build_dir,
|
||
|
'android-build',
|
||
|
'build',
|
||
|
'lib.linux{}-{}-{}'.format(
|
||
|
'2' if self.recipe.version[0] == '2' else '',
|
||
|
self.arch.command_prefix.split('-')[0],
|
||
|
self.recipe.major_minor_version_string
|
||
|
))
|
||
|
expected_sp_paths = [
|
||
|
modules_build_dir,
|
||
|
join(recipe_build_dir, 'Lib'),
|
||
|
self.ctx.get_python_install_dir(self.arch.arch),
|
||
|
]
|
||
|
for n, (sp_call, kw) in enumerate(mock_subprocess.call_args_list):
|
||
|
self.assertEqual(sp_call[0][-1], expected_sp_paths[n])
|
||
|
|
||
|
# we expect two calls to `walk_valid_filens`
|
||
|
self.assertEqual(len(mock_walk.call_args_list), 2)
|
||
|
|
||
|
mock_sh_zip.assert_called()
|
||
|
mock_sh_cp.assert_called()
|
||
|
mock_sh_find.assert_called()
|
||
|
mock_makedirs.assert_called()
|
||
|
mock_chdir.assert_called()
|
||
|
mock_copyfile.assert_called()
|
||
|
mock_copystat.assert_called()
|