From ece574898242109795b1d67d8638d8c439f32e94 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Wed, 24 Jun 2020 10:36:21 +0200 Subject: [PATCH] :white_check_mark: Unit test ios target - basic ios target test - refactors existing android test for code sharing - changes `call_build_package()` to simple function Grows `buildozer/targets/ios.py` coverage from 11% to 24%. This setups the canvas for more tests to come later. --- buildozer/targets/ios.py | 5 +- tests/targets/test_android.py | 227 ++++++++++++++-------------------- tests/targets/test_ios.py | 80 ++++++++++++ tests/targets/utils.py | 54 ++++++++ 4 files changed, 228 insertions(+), 138 deletions(-) create mode 100644 tests/targets/test_ios.py create mode 100644 tests/targets/utils.py diff --git a/buildozer/targets/ios.py b/buildozer/targets/ios.py index 7efcbc8..b588d97 100644 --- a/buildozer/targets/ios.py +++ b/buildozer/targets/ios.py @@ -3,9 +3,6 @@ iOS target, based on kivy-ios project ''' import sys -if sys.platform != 'darwin': - raise NotImplementedError('Windows platform not yet working for Android') - import plistlib from buildozer import BuildozerCommandException from buildozer.target import Target, no_config @@ -64,6 +61,8 @@ class TargetIos(Target): targetname = "ios" def check_requirements(self): + if sys.platform != "darwin": + raise NotImplementedError("Only macOS is supported for iOS target") checkbin = self.buildozer.checkbin cmd = self.buildozer.cmd executable = sys.executable or 'python' diff --git a/tests/targets/test_android.py b/tests/targets/test_android.py index d7e8a41..4f64567 100644 --- a/tests/targets/test_android.py +++ b/tests/targets/test_android.py @@ -1,17 +1,15 @@ import os -import re import tempfile from unittest import mock import pytest -import buildozer as buildozer_module -from buildozer import Buildozer from buildozer.targets.android import TargetAndroid - - -def patch_buildozer(method): - return mock.patch("buildozer.Buildozer.{method}".format(method=method)) +from tests.targets.utils import ( + init_buildozer, + patch_buildozer, + patch_buildozer_checkbin, +) def patch_buildozer_cmd(): @@ -38,10 +36,6 @@ def patch_os_isfile(): return mock.patch("os.path.isfile") -def patch_buildozer_checkbin(): - return patch_buildozer("checkbin") - - def patch_target_android(method): return mock.patch( "buildozer.targets.android.TargetAndroid.{method}".format(method=method) @@ -52,11 +46,51 @@ def patch_platform(platform): return mock.patch("buildozer.targets.android.platform", platform) -class TestTargetAndroid: +def init_target(temp_dir, options=None): + buildozer = init_buildozer(temp_dir, "android", options) + return TargetAndroid(buildozer) - @staticmethod - def default_specfile_path(): - return os.path.join(os.path.dirname(buildozer_module.__file__), "default.spec") + +def call_build_package(target_android): + """ + Call the build_package() method of the tested TargetAndroid instance, + patching the functions that would otherwise produce side-effects. + + Return the mocked execute_build_package() method of the TargetAndroid + instance so that tests can easily check which command-line arguments + would be passed on to python-for-android's toolchain. + """ + buildozer = target_android.buildozer + expected_dist_dir = ( + '{buildozer_dir}/android/platform/build-armeabi-v7a/dists/myapp__armeabi-v7a'.format( + buildozer_dir=buildozer.buildozer_dir) + ) + + with patch_target_android('_update_libraries_references') as m_update_libraries_references, \ + patch_target_android('_generate_whitelist') as m_generate_whitelist, \ + mock.patch('buildozer.targets.android.TargetAndroid.execute_build_package') as m_execute_build_package, \ + mock.patch('buildozer.targets.android.copyfile') as m_copyfile, \ + mock.patch('buildozer.targets.android.os.listdir') as m_listdir: + m_listdir.return_value = ['30.0.0-rc2'] + target_android.build_package() + + assert m_listdir.call_count == 1 + assert m_update_libraries_references.call_args_list == [ + mock.call(expected_dist_dir) + ] + assert m_generate_whitelist.call_args_list == [mock.call(expected_dist_dir)] + assert m_copyfile.call_args_list == [ + mock.call( + '{expected_dist_dir}/bin/MyApplication-0.1-debug.apk'.format( + expected_dist_dir=expected_dist_dir + ), + '{bin_dir}/myapp-0.1-armeabi-v7a-debug.apk'.format(bin_dir=buildozer.bin_dir), + ) + ] + return m_execute_build_package + + +class TestTargetAndroid: def setup_method(self): """ @@ -71,108 +105,28 @@ class TestTargetAndroid: """ self.temp_dir.cleanup() - def init_target(self, options=None): - """ - Create a buildozer.spec file in the temporary directory and init the - Buildozer and TargetAndroid instances. - - The optional argument can be used to overwrite the config options in - the buildozer.spec file, e.g.: - - self.init_target({'title': 'Test App'}) - - will replace line 4 of the default spec file. - """ - if options is None: - options = {} - - spec_path = os.path.join(self.temp_dir.name, 'buildozer.spec') - - with open(TestTargetAndroid.default_specfile_path()) as f: - default_spec = f.readlines() - - spec = [] - for line in default_spec: - if line.strip(): - match = re.search(r'[#\s]?([a-z_\.]+)', line) - key = match and match.group(1) - if key in options: - line = '{} = {}\n'.format(key, options[key]) - - spec.append(line) - - with open(spec_path, 'w') as f: - f.writelines(spec) - - self.buildozer = Buildozer(filename=spec_path, target='android') - self.target_android = TargetAndroid(self.buildozer) - - def call_build_package(self): - """ - Call the build_package() method of the tested TargetAndroid instance, - patching the functions that would otherwise produce side-effects. - - Return the mocked execute_build_package() method of the TargetAndroid - instance so that tests can easily check which command-line arguments - would be passed on to python-for-android's toolchain. - """ - expected_dist_dir = ( - '{buildozer_dir}/android/platform/build-armeabi-v7a/dists/myapp__armeabi-v7a'.format( - buildozer_dir=self.buildozer.buildozer_dir) - ) - - with patch_target_android( - '_update_libraries_references' - ) as m_update_libraries_references, patch_target_android( - '_generate_whitelist' - ) as m_generate_whitelist, mock.patch( - 'buildozer.targets.android.TargetAndroid.execute_build_package' - ) as m_execute_build_package, mock.patch( - 'buildozer.targets.android.copyfile' - ) as m_copyfile, mock.patch( - 'buildozer.targets.android.os.listdir' - ) as m_listdir: - m_listdir.return_value = ['30.0.0-rc2'] - self.target_android.build_package() - - assert m_listdir.call_count == 1 - assert m_update_libraries_references.call_args_list == [ - mock.call(expected_dist_dir) - ] - assert m_generate_whitelist.call_args_list == [mock.call(expected_dist_dir)] - assert m_copyfile.call_args_list == [ - mock.call( - '{expected_dist_dir}/bin/MyApplication-0.1-debug.apk'.format( - expected_dist_dir=expected_dist_dir - ), - '{bin_dir}/myapp-0.1-armeabi-v7a-debug.apk'.format(bin_dir=self.buildozer.bin_dir), - ) - ] - - return m_execute_build_package - def test_init(self): """Tests init defaults.""" - self.init_target() - assert self.target_android._arch == "armeabi-v7a" - assert self.target_android._build_dir.endswith( + target_android = init_target(self.temp_dir) + buildozer = target_android.buildozer + assert target_android._arch == "armeabi-v7a" + assert target_android._build_dir.endswith( ".buildozer/android/platform/build-armeabi-v7a" ) - assert self.target_android._p4a_bootstrap == "sdl2" - assert self.target_android._p4a_cmd.endswith( + assert target_android._p4a_bootstrap == "sdl2" + assert target_android._p4a_cmd.endswith( "python -m pythonforandroid.toolchain " ) - assert self.target_android.build_mode == "debug" - assert self.target_android.buildozer == self.buildozer + assert target_android.build_mode == "debug" assert ( - self.target_android.extra_p4a_args == ( + target_android.extra_p4a_args == ( ' --color=always' ' --storage-dir="{buildozer_dir}/android/platform/build-armeabi-v7a" --ndk-api=21'.format( - buildozer_dir=self.buildozer.buildozer_dir) + buildozer_dir=buildozer.buildozer_dir) ) ) - assert self.target_android.p4a_apk_cmd == "apk --debug --bootstrap=sdl2" - assert self.target_android.platform_update is False + assert target_android.p4a_apk_cmd == "apk --debug --bootstrap=sdl2" + assert target_android.platform_update is False def test_init_positional_buildozer(self): """Positional `buildozer` argument is required.""" @@ -184,18 +138,18 @@ class TestTargetAndroid: def test_sdkmanager(self): """Tests the _sdkmanager() method.""" - self.init_target() + target_android = init_target(self.temp_dir) kwargs = {} with patch_buildozer_cmd() as m_cmd, patch_buildozer_cmd_expect() as m_cmd_expect, patch_os_isfile() as m_isfile: m_isfile.return_value = True - assert m_cmd.return_value == self.target_android._sdkmanager(**kwargs) + assert m_cmd.return_value == target_android._sdkmanager(**kwargs) assert m_cmd.call_count == 1 assert m_cmd_expect.call_count == 0 assert m_isfile.call_count == 1 kwargs = {"return_child": True} with patch_buildozer_cmd() as m_cmd, patch_buildozer_cmd_expect() as m_cmd_expect, patch_os_isfile() as m_isfile: m_isfile.return_value = True - assert m_cmd_expect.return_value == self.target_android._sdkmanager( + assert m_cmd_expect.return_value == target_android._sdkmanager( **kwargs ) assert m_cmd.call_count == 0 @@ -204,43 +158,44 @@ class TestTargetAndroid: def test_check_requirements(self): """Basic tests for the check_requirements() method.""" - self.init_target() - assert not hasattr(self.target_android, "adb_cmd") - assert not hasattr(self.target_android, "javac_cmd") - assert "PATH" not in self.buildozer.environ + target_android = init_target(self.temp_dir) + buildozer = target_android.buildozer + assert not hasattr(target_android, "adb_cmd") + assert not hasattr(target_android, "javac_cmd") + assert "PATH" not in buildozer.environ with patch_buildozer_checkbin() as m_checkbin: - self.target_android.check_requirements() + target_android.check_requirements() assert m_checkbin.call_args_list == [ mock.call("Git (git)", "git"), mock.call("Cython (cython)", "cython"), mock.call("Java compiler (javac)", "javac"), mock.call("Java keytool (keytool)", "keytool"), ] - assert self.target_android.adb_cmd.endswith( + assert target_android.adb_cmd.endswith( ".buildozer/android/platform/android-sdk/platform-tools/adb" ) - assert self.target_android.javac_cmd == "javac" - assert self.target_android.keytool_cmd == "keytool" - assert "PATH" in self.buildozer.environ + assert target_android.javac_cmd == "javac" + assert target_android.keytool_cmd == "keytool" + assert "PATH" in buildozer.environ def test_check_configuration_tokens(self): """Basic tests for the check_configuration_tokens() method.""" - self.init_target() + target_android = init_target(self.temp_dir) with mock.patch( "buildozer.targets.android.Target.check_configuration_tokens" ) as m_check_configuration_tokens: - self.target_android.check_configuration_tokens() + target_android.check_configuration_tokens() assert m_check_configuration_tokens.call_args_list == [mock.call([])] @pytest.mark.parametrize("platform", ["linux", "darwin"]) def test_install_android_sdk(self, platform): """Basic tests for the _install_android_sdk() method.""" - self.init_target() + target_android = init_target(self.temp_dir) with patch_buildozer_file_exists() as m_file_exists, patch_buildozer_download() as m_download: m_file_exists.return_value = True - sdk_dir = self.target_android._install_android_sdk() + sdk_dir = target_android._install_android_sdk() assert m_file_exists.call_args_list == [ - mock.call(self.target_android.android_sdk_dir) + mock.call(target_android.android_sdk_dir) ] assert m_download.call_args_list == [] assert sdk_dir.endswith(".buildozer/android/platform/android-sdk") @@ -249,9 +204,9 @@ class TestTargetAndroid: patch_buildozer_file_extract() as m_file_extract, \ patch_platform(platform): m_file_exists.return_value = False - sdk_dir = self.target_android._install_android_sdk() + sdk_dir = target_android._install_android_sdk() assert m_file_exists.call_args_list == [ - mock.call(self.target_android.android_sdk_dir) + mock.call(target_android.android_sdk_dir) ] platform_map = {"linux": "linux", "darwin": "mac"} platform = platform_map[platform] @@ -268,8 +223,9 @@ class TestTargetAndroid: def test_build_package(self): """Basic tests for the build_package() method.""" - self.init_target() - m_execute_build_package = self.call_build_package() + target_android = init_target(self.temp_dir) + buildozer = target_android.buildozer + m_execute_build_package = call_build_package(target_android) assert m_execute_build_package.call_args_list == [ mock.call( [ @@ -278,7 +234,7 @@ class TestTargetAndroid: ("--package", "org.test.myapp"), ("--minsdk", "21"), ("--ndk-api", "21"), - ("--private", "{buildozer_dir}/android/app".format(buildozer_dir=self.buildozer.buildozer_dir)), + ("--private", "{buildozer_dir}/android/app".format(buildozer_dir=buildozer.buildozer_dir)), ("--android-entrypoint", "org.kivy.android.PythonActivity"), ("--android-apptheme", "@android:style/Theme.NoTitleBar"), ("--orientation", "portrait"), @@ -290,10 +246,11 @@ class TestTargetAndroid: def test_numeric_version(self): """The `android.numeric_version` config should be passed to `build_package()`.""" - self.init_target({ + target_android = init_target(self.temp_dir, { "android.numeric_version": "1234" }) - m_execute_build_package = self.call_build_package() + buildozer = target_android.buildozer + m_execute_build_package = call_build_package(target_android) assert m_execute_build_package.call_args_list == [ mock.call( [ @@ -302,7 +259,7 @@ class TestTargetAndroid: ("--package", "org.test.myapp"), ("--minsdk", "21"), ("--ndk-api", "21"), - ("--private", "{buildozer_dir}/android/app".format(buildozer_dir=self.buildozer.buildozer_dir)), + ("--private", "{buildozer_dir}/android/app".format(buildozer_dir=buildozer.buildozer_dir)), ("--android-entrypoint", "org.kivy.android.PythonActivity"), ("--android-apptheme", "@android:style/Theme.NoTitleBar"), ("--orientation", "portrait"), @@ -323,11 +280,11 @@ class TestTargetAndroid: with open(filters_path, 'w') as f: f.write('') - self.init_target({ + target_android = init_target(self.temp_dir, { 'android.manifest.intent_filters': 'filters.xml' }) - - m_execute_build_package = self.call_build_package() + buildozer = target_android.buildozer + m_execute_build_package = call_build_package(target_android) assert m_execute_build_package.call_args_list == [ mock.call( @@ -337,7 +294,7 @@ class TestTargetAndroid: ('--package', 'org.test.myapp'), ('--minsdk', '21'), ('--ndk-api', '21'), - ('--private', '{buildozer_dir}/android/app'.format(buildozer_dir=self.buildozer.buildozer_dir)), + ('--private', '{buildozer_dir}/android/app'.format(buildozer_dir=buildozer.buildozer_dir)), ('--android-entrypoint', 'org.kivy.android.PythonActivity'), ('--android-apptheme', '@android:style/Theme.NoTitleBar'), ('--orientation', 'portrait'), diff --git a/tests/targets/test_ios.py b/tests/targets/test_ios.py new file mode 100644 index 0000000..7ef1b04 --- /dev/null +++ b/tests/targets/test_ios.py @@ -0,0 +1,80 @@ +import sys +import tempfile +from unittest import mock + +import pytest + +from buildozer.targets.ios import TargetIos +from tests.targets.utils import init_buildozer, patch_buildozer_checkbin + + +def init_target(temp_dir, options=None): + buildozer = init_buildozer(temp_dir, "ios", options) + return TargetIos(buildozer) + + +@pytest.mark.skipif( + sys.platform != "darwin", reason="Only macOS is supported for target iOS" +) +class TestTargetIos: + def setup_method(self): + """ + Create a temporary directory that will contain the spec file and will + serve as the root_dir. + """ + self.temp_dir = tempfile.TemporaryDirectory() + + def tear_method(self): + """ + Remove the temporary directory created in self.setup_method. + """ + self.temp_dir.cleanup() + + def test_init(self): + """Tests init defaults.""" + target = init_target(self.temp_dir) + assert target.targetname == "ios" + assert target.code_signing_allowed == "CODE_SIGNING_ALLOWED=NO" + assert target.build_mode == "debug" + assert target.platform_update is False + + def test_check_requirements(self): + """Basic tests for the check_requirements() method.""" + target = init_target(self.temp_dir) + buildozer = target.buildozer + assert not hasattr(target, "adb_cmd") + assert not hasattr(target, "javac_cmd") + assert "PATH" not in buildozer.environ + with patch_buildozer_checkbin() as m_checkbin: + target.check_requirements() + assert m_checkbin.call_args_list == [ + mock.call("Xcode xcodebuild", "xcodebuild"), + mock.call("Xcode xcode-select", "xcode-select"), + mock.call("Git git", "git"), + mock.call("Cython cython", "cython"), + mock.call("pkg-config", "pkg-config"), + mock.call("autoconf", "autoconf"), + mock.call("automake", "automake"), + mock.call("libtool", "libtool"), + ] + assert target._toolchain_cmd.endswith("toolchain.py ") + assert target._xcodebuild_cmd == "xcodebuild " + + def test_check_configuration_tokens(self): + """Basic tests for the check_configuration_tokens() method.""" + target = init_target(self.temp_dir, {"ios.codesign.allowed": "yes"}) + with mock.patch( + "buildozer.targets.android.Target.check_configuration_tokens" + ) as m_check_configuration_tokens, mock.patch( + "buildozer.targets.ios.TargetIos._get_available_identities" + ) as m_get_available_identities: + target.check_configuration_tokens() + assert m_get_available_identities.call_args_list == [mock.call()] + assert m_check_configuration_tokens.call_args_list == [ + mock.call( + [ + '[app] "ios.codesign.debug" key missing, you must give a certificate name to use.', + '[app] "ios.codesign.release" key missing, you must give a certificate name to use.', + ] + ) + ] diff --git a/tests/targets/utils.py b/tests/targets/utils.py new file mode 100644 index 0000000..971bfe9 --- /dev/null +++ b/tests/targets/utils.py @@ -0,0 +1,54 @@ +import os +import re +from unittest import mock + +import buildozer as buildozer_module +from buildozer import Buildozer + + +def patch_buildozer(method): + return mock.patch("buildozer.Buildozer.{method}".format(method=method)) + + +def patch_buildozer_checkbin(): + return patch_buildozer("checkbin") + + +def default_specfile_path(): + return os.path.join(os.path.dirname(buildozer_module.__file__), "default.spec") + + +def init_buildozer(temp_dir, target, options=None): + """ + Create a buildozer.spec file in the temporary directory and init the + Buildozer instance. + + The optional argument can be used to overwrite the config options in + the buildozer.spec file, e.g.: + + init_buildozer({'title': 'Test App'}) + + will replace line 4 of the default spec file. + """ + if options is None: + options = {} + + spec_path = os.path.join(temp_dir.name, "buildozer.spec") + + with open(default_specfile_path()) as f: + default_spec = f.readlines() + + spec = [] + for line in default_spec: + if line.strip(): + match = re.search(r"[#\s]?([a-z_\.]+)", line) + key = match and match.group(1) + if key in options: + line = "{} = {}\n".format(key, options[key]) + + spec.append(line) + + with open(spec_path, "w") as f: + f.writelines(spec) + + return Buildozer(filename=spec_path, target=target)