Skip to content

Commit be7abe9

Browse files
authored
A bunch of tests for library recipes (#1982)
The main idea is to test as many recipes as we can with the less code possible and without creating any file/directory so our tests can be performed as fast as possible (all this tests will only add between 2 and 3 seconds to our CI tests and will cover almost 100% of the code for each tested recipe) To do so, we create a couple of modules: tests.recipe_ctx: allow us to create a proper Context to test our recipes tests.recipe_lib_test: which holds some base classes to be used to test a recipe depending on the build method used. For starters we introduce two kind of base classes: BaseTestForMakeRecipe: To test an standard library build (this will iinclude the recipes which requires the classical build commandsconfigure/make) BaseTestForCmakeRecipe: To test an library recipe which is compiled with cmake We also refactor the existing recipes tests, so we can remove some lines in there...the ones that creates a Context. * [test] Add module `tests.recipe_ctx` A helper module to test recipes. Here we will initialize a `Context` to test recipes. * [test] Refactor `setUp/tearDown` for `test_icu` * [test] Refactor `setUp/tearDown` for `test_pyicu` * [test] Refactor `setUp` for `test_reportlab` * [test] Refactor `setUp` for `test_gevent` * [test] Add module `tests.recipe_lib_test` A helper module to test recipes which will allow to test any recipe using `configure/make` commands. * [test] Add test for `libffi` * [test] Add test for `libexpat` * [test] Add test for `libcurl` * [test] Add test for `libiconv` * [test] Add test for `libogg` * [test] Add test for `libpq` * [test] Add test for `libsecp256k1` * [test] Add test for `libshine` * [test] Add test for `libvorbis` * [test] Add test for `libx264` * [test] Add test for `libxml2` * [test] Add test for `libxslt` * [test] Add test for `png` * [test] Add test for `freetype` * [test] Add test for `harfbuzz` * [test] Add test for `openssl` * [test] Add `BaseTestForCmakeRecipe` * [test] Add test for `jpeg` and clean code We can remove the `get_recipe_env` because the environment that we use is already using a clang as default compiler (not the case when we migrated the jpeg recipe to use `cmake`)...so we can do a little refactor :) * [test] Add test for `snappy` * [test] Add test for `leveldb` * [test] Add test for `libgeos` * [test] Add test for `libmysqlclient` * [test] Add test for `openal` * [test] Make the `super` calls Python3 style * [test] Move mock tests outside context manager... Because there is no need to do it there. Also rewrote the inline comments.
1 parent 7f18efa commit be7abe9

29 files changed

+665
-144
lines changed

pythonforandroid/recipes/jpeg/__init__.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from pythonforandroid.logger import shprint
33
from pythonforandroid.util import current_directory
44
from os.path import join
5-
from os import environ, uname
65
import sh
76

87

@@ -35,10 +34,9 @@ def build_arch(self, arch):
3534
'-DCMAKE_POSITION_INDEPENDENT_CODE=1',
3635
'-DCMAKE_ANDROID_ARCH_ABI={arch}'.format(arch=arch.arch),
3736
'-DCMAKE_ANDROID_NDK=' + self.ctx.ndk_dir,
38-
'-DCMAKE_C_COMPILER={toolchain}/bin/clang'.format(
39-
toolchain=env['TOOLCHAIN']),
40-
'-DCMAKE_CXX_COMPILER={toolchain}/bin/clang++'.format(
41-
toolchain=env['TOOLCHAIN']),
37+
'-DCMAKE_C_COMPILER={cc}'.format(cc=arch.get_clang_exe()),
38+
'-DCMAKE_CXX_COMPILER={cc_plus}'.format(
39+
cc_plus=arch.get_clang_exe(plus_plus=True)),
4240
'-DCMAKE_BUILD_TYPE=Release',
4341
'-DCMAKE_INSTALL_PREFIX=./install',
4442
'-DCMAKE_TOOLCHAIN_FILE=' + toolchain_file,
@@ -54,16 +52,5 @@ def build_arch(self, arch):
5452
_env=env)
5553
shprint(sh.make, _env=env)
5654

57-
def get_recipe_env(self, arch=None, with_flags_in_cc=False):
58-
env = environ.copy()
59-
60-
build_platform = '{system}-{machine}'.format(
61-
system=uname()[0], machine=uname()[-1]).lower()
62-
env['TOOLCHAIN'] = join(self.ctx.ndk_dir, 'toolchains/llvm/'
63-
'prebuilt/{build_platform}'.format(
64-
build_platform=build_platform))
65-
66-
return env
67-
6855

6956
recipe = JpegRecipe()

tests/recipes/recipe_ctx.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import os
2+
3+
from pythonforandroid.bootstrap import Bootstrap
4+
from pythonforandroid.distribution import Distribution
5+
from pythonforandroid.recipe import Recipe
6+
from pythonforandroid.build import Context
7+
from pythonforandroid.archs import ArchAarch_64
8+
9+
10+
class RecipeCtx:
11+
"""
12+
An base class for unit testing a recipe. This will create a context so we
13+
can test any recipe using this context. Implement `setUp` and `tearDown`
14+
methods used by unit testing.
15+
"""
16+
17+
ctx = None
18+
arch = None
19+
recipe = None
20+
21+
recipe_name = ""
22+
"The name of the recipe to test."
23+
24+
recipes = []
25+
"""A List of recipes to pass to `Distribution.get_distribution`. Should
26+
contain the target recipe to test as well as a python recipe."""
27+
recipe_build_order = []
28+
"""A recipe_build_order which should take into account the recipe we want
29+
to test as well as the possible dependant recipes"""
30+
31+
def setUp(self):
32+
self.ctx = Context()
33+
self.ctx.ndk_api = 21
34+
self.ctx.android_api = 27
35+
self.ctx._sdk_dir = "/opt/android/android-sdk"
36+
self.ctx._ndk_dir = "/opt/android/android-ndk"
37+
self.ctx.setup_dirs(os.getcwd())
38+
self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx)
39+
self.ctx.bootstrap.distribution = Distribution.get_distribution(
40+
self.ctx, name="sdl2", recipes=self.recipes
41+
)
42+
self.ctx.recipe_build_order = self.recipe_build_order
43+
self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx)
44+
self.arch = ArchAarch_64(self.ctx)
45+
self.ctx.ndk_platform = (
46+
f"{self.ctx._ndk_dir}/platforms/"
47+
f"android-{self.ctx.ndk_api}/{self.arch.platform_dir}"
48+
)
49+
self.recipe = Recipe.get_recipe(self.recipe_name, self.ctx)
50+
51+
def tearDown(self):
52+
self.ctx = None

tests/recipes/recipe_lib_test.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from unittest import mock
2+
from tests.recipes.recipe_ctx import RecipeCtx
3+
4+
5+
class BaseTestForMakeRecipe(RecipeCtx):
6+
"""
7+
An unittest for testing any recipe using the standard build commands
8+
(`configure/make`).
9+
10+
.. note:: Note that Some cmake recipe may need some more specific testing
11+
...but this should cover the basics.
12+
"""
13+
14+
recipe_name = None
15+
recipes = ["python3", "kivy"]
16+
recipe_build_order = ["hostpython3", "python3", "sdl2", "kivy"]
17+
expected_compiler = (
18+
"{android_ndk}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang"
19+
)
20+
21+
sh_command_calls = ["./configure"]
22+
"""The expected commands that the recipe runs via `sh.command`."""
23+
24+
extra_env_flags = {}
25+
"""
26+
This must be a dictionary containing pairs of key (env var) and value.
27+
"""
28+
29+
def __new__(cls, *args):
30+
obj = super().__new__(cls)
31+
if obj.recipe_name is not None:
32+
print(f"We are testing recipe: {obj.recipe_name}")
33+
obj.recipes.append(obj.recipe_name)
34+
obj.recipe_build_order.insert(1, obj.recipe_name)
35+
return obj
36+
37+
@mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices")
38+
@mock.patch("pythonforandroid.build.ensure_dir")
39+
@mock.patch("pythonforandroid.archs.glob")
40+
@mock.patch("pythonforandroid.archs.find_executable")
41+
def test_get_recipe_env(
42+
self,
43+
mock_find_executable,
44+
mock_glob,
45+
mock_ensure_dir,
46+
mock_check_recipe_choices,
47+
):
48+
"""
49+
Test that get_recipe_env contains some expected arch flags and that
50+
some internal methods has been called.
51+
"""
52+
mock_find_executable.return_value = self.expected_compiler.format(
53+
android_ndk=self.ctx._ndk_dir
54+
)
55+
mock_glob.return_value = ["llvm"]
56+
mock_check_recipe_choices.return_value = sorted(
57+
self.ctx.recipe_build_order
58+
)
59+
60+
# make sure the arch flags are in env
61+
env = self.recipe.get_recipe_env(self.arch)
62+
for flag in self.arch.arch_cflags:
63+
self.assertIn(flag, env["CFLAGS"])
64+
self.assertIn(
65+
f"-target {self.arch.target}",
66+
env["CFLAGS"],
67+
)
68+
69+
for flag, value in self.extra_env_flags.items():
70+
self.assertIn(value, env[flag])
71+
72+
# make sure that the mocked methods are actually called
73+
mock_glob.assert_called()
74+
mock_ensure_dir.assert_called()
75+
mock_find_executable.assert_called()
76+
mock_check_recipe_choices.assert_called()
77+
78+
@mock.patch("pythonforandroid.util.chdir")
79+
@mock.patch("pythonforandroid.build.ensure_dir")
80+
@mock.patch("pythonforandroid.archs.glob")
81+
@mock.patch("pythonforandroid.archs.find_executable")
82+
def test_build_arch(
83+
self,
84+
mock_find_executable,
85+
mock_glob,
86+
mock_ensure_dir,
87+
mock_current_directory,
88+
):
89+
mock_find_executable.return_value = self.expected_compiler.format(
90+
android_ndk=self.ctx._ndk_dir
91+
)
92+
mock_glob.return_value = ["llvm"]
93+
94+
# Since the following mocks are dynamic,
95+
# we mock it inside a Context Manager
96+
with mock.patch(
97+
f"pythonforandroid.recipes.{self.recipe_name}.sh.Command"
98+
) as mock_sh_command, mock.patch(
99+
f"pythonforandroid.recipes.{self.recipe_name}.sh.make"
100+
) as mock_make:
101+
self.recipe.build_arch(self.arch)
102+
103+
# make sure that the mocked methods are actually called
104+
for command in self.sh_command_calls:
105+
self.assertIn(
106+
mock.call(command),
107+
mock_sh_command.mock_calls,
108+
)
109+
mock_make.assert_called()
110+
mock_glob.assert_called()
111+
mock_ensure_dir.assert_called()
112+
mock_current_directory.assert_called()
113+
mock_find_executable.assert_called()
114+
115+
116+
class BaseTestForCmakeRecipe(BaseTestForMakeRecipe):
117+
"""
118+
An unittest for testing any recipe using `cmake`. It inherits from
119+
`BaseTestForMakeRecipe` but we override the build method to match the cmake
120+
build method.
121+
122+
.. note:: Note that Some cmake recipe may need some more specific testing
123+
...but this should cover the basics.
124+
"""
125+
126+
@mock.patch("pythonforandroid.util.chdir")
127+
@mock.patch("pythonforandroid.build.ensure_dir")
128+
@mock.patch("pythonforandroid.archs.glob")
129+
@mock.patch("pythonforandroid.archs.find_executable")
130+
def test_build_arch(
131+
self,
132+
mock_find_executable,
133+
mock_glob,
134+
mock_ensure_dir,
135+
mock_current_directory,
136+
):
137+
mock_find_executable.return_value = self.expected_compiler.format(
138+
android_ndk=self.ctx._ndk_dir
139+
)
140+
mock_glob.return_value = ["llvm"]
141+
142+
# Since the following mocks are dynamic,
143+
# we mock it inside a Context Manager
144+
with mock.patch(
145+
f"pythonforandroid.recipes.{self.recipe_name}.sh.make"
146+
) as mock_make, mock.patch(
147+
f"pythonforandroid.recipes.{self.recipe_name}.sh.cmake"
148+
) as mock_cmake:
149+
self.recipe.build_arch(self.arch)
150+
151+
# make sure that the mocked methods are actually called
152+
mock_cmake.assert_called()
153+
mock_make.assert_called()
154+
mock_glob.assert_called()
155+
mock_ensure_dir.assert_called()
156+
mock_current_directory.assert_called()
157+
mock_find_executable.assert_called()

tests/recipes/test_freetype.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import unittest
2+
from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe
3+
4+
5+
class TestFreetypeRecipe(BaseTestForMakeRecipe, unittest.TestCase):
6+
"""
7+
An unittest for recipe :mod:`~pythonforandroid.recipes.freetype`
8+
"""
9+
recipe_name = "freetype"
10+
sh_command_calls = ["./configure"]

tests/recipes/test_gevent.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
11
import unittest
22
from mock import patch
3-
from pythonforandroid.archs import ArchARMv7_a
4-
from pythonforandroid.build import Context
5-
from pythonforandroid.recipe import Recipe
3+
from tests.recipes.recipe_ctx import RecipeCtx
64

75

8-
class TestGeventRecipe(unittest.TestCase):
6+
class TestGeventRecipe(RecipeCtx, unittest.TestCase):
97

10-
def setUp(self):
11-
"""
12-
Setups recipe and context.
13-
"""
14-
self.context = Context()
15-
self.context.ndk_api = 21
16-
self.context.android_api = 27
17-
self.arch = ArchARMv7_a(self.context)
18-
self.recipe = Recipe.get_recipe('gevent', self.context)
8+
recipe_name = "gevent"
199

2010
def test_get_recipe_env(self):
2111
"""

tests/recipes/test_harfbuzz.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import unittest
2+
from tests.recipes.recipe_lib_test import BaseTestForMakeRecipe
3+
4+
5+
class TestHarfbuzzRecipe(BaseTestForMakeRecipe, unittest.TestCase):
6+
"""
7+
An unittest for recipe :mod:`~pythonforandroid.recipes.harfbuzz`
8+
"""
9+
recipe_name = "harfbuzz"
10+
sh_command_calls = ["./configure"]

0 commit comments

Comments
 (0)