diff --git a/changes/737.feature.md b/changes/737.feature.md new file mode 100644 index 000000000..88f811c62 --- /dev/null +++ b/changes/737.feature.md @@ -0,0 +1 @@ +Android emulator system images and image types can now be selected when creating a new emulator, with options ranging from Android 26 to the latest available version. diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py index 6044c201b..f0215ab97 100644 --- a/src/briefcase/integrations/android_sdk.py +++ b/src/briefcase/integrations/android_sdk.py @@ -10,7 +10,7 @@ from datetime import datetime from pathlib import Path -from briefcase.config import PEP508_NAME_RE +from briefcase.config import PEP508_NAME_RE, FinalizedAppConfig from briefcase.console import Console from briefcase.exceptions import ( BriefcaseCommandError, @@ -24,6 +24,7 @@ from briefcase.integrations.subprocess import SubprocessArgT DEVICE_NOT_FOUND = re.compile(r"^error: device '[^']*' not found") +ANDROID_MIN_OS_VERSION = 24 def create_avd_validator(emulators): @@ -40,6 +41,49 @@ def _validate_avd_name(avd): return _validate_avd_name +def _parse_system_image(package: str): + """Parse a system image package identifier into its components. + + :param package: e.g. ``"system-images;android-31;default;x86_64"`` + :returns: A tuple of (api_level, tag, abi) or None if invalid. + """ + parts = package.split(";") + if len(parts) != 4 or parts[0] != "system-images": + return None + return parts[1], parts[2], parts[3] + + +def _api_level_sort_key(api_level: str) -> tuple: + """Sort key for API level strings, ordering numeric levels (and their ext suffixes) + numerically. Non-numeric levels sort after all numeric ones. + + e.g. "android-34" -> (0, 34.0, 0) "android-34-ext9" -> (0, 34.0, 9) + "android-36.0-Baklava" -> (0, 36.0, 0) "android-CANARY" -> (1, "android- + CANARY") + """ + value = api_level.split("-", 1)[ + 1 + ] # "34", "34-ext9", "36.1", "36.0-Baklava", "CANARY" + base, _, ext = value.partition("-ext") + base = base.split("-", 1)[ + 0 + ] # strip trailing "-Name" suffix, e.g. "36.0-Baklava" -> "36.0" + try: + return 0, float(base), int(ext) if ext else 0 + except ValueError: + return 1, api_level + + +def min_api_level(app: FinalizedAppConfig) -> int: + """The minimum API level to use for the app, as an int. + + ``min_os_version`` may be configured as a string or an int, so it's + coerced to int here to avoid a TypeError when compared against + numeric API levels. + """ + return int(getattr(app, "min_os_version", ANDROID_MIN_OS_VERSION)) + + class AndroidDeviceNotAuthorized(BriefcaseCommandError): def __init__(self, device): self.device = device @@ -199,9 +243,20 @@ def DEFAULT_DEVICE_TYPE(self) -> str: def DEFAULT_DEVICE_SKIN(self) -> str: return "pixel_7_pro" + @property + def DEFAULT_API_LEVEL(self) -> str: + return "android-31" + + @property + def DEFAULT_TAG(self) -> str: + return "default" + @property def DEFAULT_SYSTEM_IMAGE(self) -> str: - return f"system-images;android-31;default;{self.emulator_abi}" + return ( + f"system-images;{self.DEFAULT_API_LEVEL}" + f";{self.DEFAULT_TAG};{self.emulator_abi}" + ) @classmethod def sdk_path_from_env(cls, tools: ToolCache) -> tuple[str | None, str | None]: @@ -719,6 +774,44 @@ def verify_avd(self, avd: str): except KeyError: self.tools.console.debug(f"Device {avd!r} doesn't define a skin.") + def list_available_system_images(self, min_api_level: int) -> list[str]: + """Returns a sorted list of system image package identifiers available for the + current architecture and minimum Android version. + + e.g., ``{"system-images;android-31;default;x86_64"}`` + + :param min_api_level: The minimum Android API level to include. + """ + + try: + output = self.tools.subprocess.check_output( + [self.sdkmanager_path, "--list"], + env=self.env, + ) + except subprocess.CalledProcessError as e: + raise BriefcaseCommandError( + "Unable to invoke the Android SDK manager" + ) from e + + images = [] + for line in output.splitlines(): + package = line.split("|")[0].strip() + parsed = _parse_system_image(package) + if parsed is None: + continue + api_level, _, abi = parsed + if abi != self.emulator_abi: + continue + api_level_str = api_level.split("-")[1] # "android-31" -> "31" + try: + if int(api_level_str.split(".")[0]) < min_api_level: + continue + except ValueError: + # Non-numeric API level (e.g. CANARY, CinnamonBun) always include. + pass + images.append(package) + return sorted(set(images)) + def list_installed_system_images(self) -> set[str]: """Returns a set of installed system image package identifiers. @@ -1099,9 +1192,10 @@ def select_target_device( return device, name, avd - def create_emulator(self) -> str: + def create_emulator(self, app: FinalizedAppConfig) -> str: """Create a new Android emulator. + :param app: The config object for the app. :returns: The AVD of the newly created emulator. """ # Get the list of existing emulators @@ -1132,8 +1226,49 @@ def create_emulator(self) -> str: device_type = self.DEFAULT_DEVICE_TYPE skin = self.DEFAULT_DEVICE_SKIN - # TODO: Provide a list of options for system images. - system_image = self.DEFAULT_SYSTEM_IMAGE + # Get available images, raise an error if none found. + available_images = self.list_available_system_images( + min_api_level=min_api_level(app) + ) + if not available_images: + raise BriefcaseCommandError( + f"""\ +No Android system images are available for your architecture +({self.emulator_abi}). + +This may be caused by a network connectivity issue or an unsupported +architecture. Check your network connection and re-run `briefcase run android`. +""" + ) + + # Parse available images once for use in both selection questions. + parsed_images = [_parse_system_image(img) for img in available_images] + + # Ask the user to select an API level. + api_levels = sorted( + {api_level for api_level, _, abi in parsed_images}, + key=_api_level_sort_key, + ) + api_level = self.tools.console.selection_question( + intro="Select the API level for the emulator:", + description="API level", + options=api_levels, + default=self.DEFAULT_API_LEVEL, + ) + + # Ask the user to select a tag for the chosen API level. + tags = sorted( + {tag for level, tag, _ in parsed_images if level == api_level}, + key=lambda x: (0 if x == "default" else 1, x), + ) + tag = self.tools.console.selection_question( + intro="Select the system image tag:", + description="Tag", + options=tags, + default=self.DEFAULT_TAG if self.DEFAULT_TAG in tags else tags[0], + ) + + system_image = f"system-images;{api_level};{tag};{self.emulator_abi}" self._create_emulator( avd=avd, diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py index bacfb0c78..c95060255 100644 --- a/src/briefcase/platforms/android/gradle.py +++ b/src/briefcase/platforms/android/gradle.py @@ -25,7 +25,11 @@ DebuggerConnectionMode, ) from briefcase.exceptions import BriefcaseCommandError -from briefcase.integrations.android_sdk import ADB, AndroidSDK +from briefcase.integrations.android_sdk import ( + ADB, + AndroidSDK, + min_api_level, +) from briefcase.integrations.subprocess import SubprocessArgT if TYPE_CHECKING: @@ -222,11 +226,14 @@ def output_format_template_context(self, app: FinalizedAppConfig): "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0", ] + min_os_version = min_api_level(app) + return { "version_code": version_code, "safe_formal_name": safe_formal_name(app.formal_name), "build_gradle_dependencies": {"implementation": dependencies}, "ndk": {"abi_filters": getattr(app, "android_abis", None)}, + "min_os_version": min_os_version, } def permissions_context( @@ -487,7 +494,7 @@ def run_app( # then start it. if device is None: if avd is None: - avd = self.tools.android_sdk.create_emulator() + avd = self.tools.android_sdk.create_emulator(app) else: # Ensure the system image for the requested emulator is available. # This step is only needed if the AVD already existed; you have to diff --git a/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py index 650fcc20d..24dea04bc 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py +++ b/tests/integrations/android_sdk/AndroidSDK/test__create_emulator.py @@ -26,6 +26,13 @@ def android_sdk(android_sdk) -> AndroidSDK: "idleEmulator", ] ) + # Mock available system images + android_sdk.list_available_system_images = MagicMock( + return_value=[ + "system-images;android-31;default;x86_64", + "system-images;android-34;default;x86_64", + ] + ) return android_sdk @@ -311,8 +318,12 @@ def test_default_name(mock_tools, android_sdk, tmp_path): # This test doesn't validate most of the test process; # it only checks that the emulator is created with the default name. - # User provides no input; default name will be used - mock_tools.console.values = [""] + # User provides no input; default name, system image and image type will be used. + mock_tools.console.values = [ + "", + "", + "", + ] # Mock the initial output of an AVD config file. avd_config_path = tmp_path / "home/.android/avd/beePhone.avd/config.ini" @@ -320,8 +331,12 @@ def test_default_name(mock_tools, android_sdk, tmp_path): with avd_config_path.open("w", encoding="utf-8") as f: f.write("hw.device.name=pixel\n") + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + # Create the emulator - avd = android_sdk.create_emulator() + avd = android_sdk.create_emulator(app) # The expected device AVD was created. assert avd == "beePhone" @@ -340,8 +355,12 @@ def test_default_name_with_collisions(mock_tools, android_sdk, tmp_path): "beePhone", ] ) - # User provides no input; default name will be used - mock_tools.console.values = [""] + # User provides no input; default name, system image and image type will be used. + mock_tools.console.values = [ + "", + "", + "", + ] # Mock the initial output of an AVD config file. avd_config_path = tmp_path / "home/.android/avd/beePhone3.avd/config.ini" @@ -349,8 +368,12 @@ def test_default_name_with_collisions(mock_tools, android_sdk, tmp_path): with avd_config_path.open("w", encoding="utf-8") as f: f.write("hw.device.name=pixel\n") + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + # Create the emulator - avd = android_sdk.create_emulator() + avd = android_sdk.create_emulator(app) # The expected device AVD was created. assert avd == "beePhone3" diff --git a/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py index 44fa1b5b5..411c5327f 100644 --- a/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py +++ b/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py @@ -2,6 +2,7 @@ import pytest +from briefcase.exceptions import BriefcaseCommandError from briefcase.integrations.android_sdk import AndroidSDK from briefcase.integrations.base import ToolCache @@ -24,6 +25,19 @@ def android_sdk(android_sdk) -> AndroidSDK: "idleEmulator", ] ) + # Mock available system images + android_sdk.list_available_system_images = MagicMock( + return_value=[ + "system-images;android-31;default;x86_64", + "system-images;android-34;default;x86_64", + "system-images;android-34;google_apis;x86_64", + "system-images;android-31;default;arm64-v8a", + "system-images;android-34;default;arm64-v8a", + "system-images;android-34;google_apis;arm64-v8a", + "system-images;android-CANARY;google_apis;x86_64", + "system-images;android-CinnamonBun;google_apis_playstore;x86_64", + ] + ) return android_sdk @@ -59,6 +73,8 @@ def test_create_emulator( "invalid name", # A name with a space "annoying!", # a name with non-alpha characters "new-emulator", # A valid name! + "2", # Android API level selection (android-34) + "1", # tag selection (default) ] # Mock the initial output of an AVD config file. @@ -70,8 +86,12 @@ def test_create_emulator( # Mock the internal emulator creation method android_sdk._create_emulator = MagicMock() + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + # Create the emulator - avd = android_sdk.create_emulator() + avd = android_sdk.create_emulator(app) # The expected device AVD was created. assert avd == "new-emulator" @@ -81,7 +101,7 @@ def test_create_emulator( avd="new-emulator", device_type="pixel", skin="pixel_7_pro", - system_image=f"system-images;android-31;default;{emulator_abi}", + system_image=f"system-images;android-34;default;{emulator_abi}", ) @@ -90,14 +110,22 @@ def test_default_name(mock_tools, android_sdk, tmp_path): # This test doesn't validate most of the test process; # it only checks that the emulator is created with the default name. - # User provides no input; default name will be used - mock_tools.console.values = [""] + # User provides no input; default name, system image and image type will be used. + mock_tools.console.values = [ + "", + "", + "", + ] # Mock the internal emulator creation method android_sdk._create_emulator = MagicMock() + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + # Create the emulator - avd = android_sdk.create_emulator() + avd = android_sdk.create_emulator(app) # The expected device AVD was created. assert avd == "beePhone" @@ -116,13 +144,91 @@ def test_default_name_with_collisions(mock_tools, android_sdk, tmp_path): "beePhone", ] ) - mock_tools.console.values = [""] + # Default emulator name, default system image and default image type selection. + mock_tools.console.values = [ + "", + "", + "", + ] # Mock the internal emulator creation method android_sdk._create_emulator = MagicMock() + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + # Create the emulator - avd = android_sdk.create_emulator() + avd = android_sdk.create_emulator(app) # The expected device AVD was created. assert avd == "beePhone3" + + +def test_system_image_selection(mock_tools, android_sdk, tmp_path): + """The user can select an Android version and image type.""" + mock_tools.console.values = [ + "", # default emulator name + "2", # select API level 34 (option 2 in the list) + "2", # tag selection (google_apis option 2 in the list) + ] + + # Mock the internal emulator creation method + android_sdk._create_emulator = MagicMock() + + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + + # Create the emulator + avd = android_sdk.create_emulator(app) + + # The expected device AVD was created. + assert avd == "beePhone" + + # The call was made to create the emulator + android_sdk._create_emulator.assert_called_once_with( + avd="beePhone", + device_type="pixel", + skin="pixel_7_pro", + system_image="system-images;android-34;google_apis;x86_64", + ) + + +def test_no_available_system_images(mock_tools, android_sdk, tmp_path): + """If no system images are available, an error is raised.""" + android_sdk.list_available_system_images = MagicMock(return_value=[]) + + # User provides a name before the error is raised + mock_tools.console.values = [""] # default emulator name + + # Create a mock app + app = MagicMock() + del app.min_os_version # ensure getattr fallback is used + + # No system image detected + with pytest.raises(BriefcaseCommandError): + android_sdk.create_emulator(app) + + +def test_create_emulator_with_min_os_version(mock_tools, android_sdk, tmp_path): + """create_emulator passes app's min_os_version to list_available_system_images.""" + mock_tools.console.values = [ + "new-emulator", # emulator name + "2", # API level selection (android-34) + "1", # tag selection (default) + ] + # Mock the initial output of an AVD config file. + avd_config_path = tmp_path / "home/.android/avd/new-emulator.avd/config.ini" + avd_config_path.parent.mkdir(parents=True) + with avd_config_path.open("w", encoding="utf-8") as f: + f.write("hw.device.name=pixel\n") + # Mock the internal emulator creation method + android_sdk._create_emulator = MagicMock() + # Create a mock app with min_os_version explicitly set + app = MagicMock() + app.min_os_version = 28 + # Create the emulator + android_sdk.create_emulator(app) + # Verify list_available_system_images was called with the app's min_os_version + android_sdk.list_available_system_images.assert_called_once_with(min_api_level=28) diff --git a/tests/integrations/android_sdk/AndroidSDK/test_list_available_system_images.py b/tests/integrations/android_sdk/AndroidSDK/test_list_available_system_images.py new file mode 100644 index 000000000..f1af42f74 --- /dev/null +++ b/tests/integrations/android_sdk/AndroidSDK/test_list_available_system_images.py @@ -0,0 +1,217 @@ +import subprocess + +import pytest + +from briefcase.exceptions import BriefcaseCommandError +from briefcase.integrations.android_sdk import ANDROID_MIN_OS_VERSION +from briefcase.integrations.base import ToolCache + + +@pytest.fixture +def mock_tools(tmp_path, mock_tools) -> ToolCache: + # Lock to macOS x86_64 so architecture-independent tests are deterministic. + # Architecture filtering is tested separately in test_list_available_system_images_other_abi. + mock_tools.host_os = "Darwin" + mock_tools.host_arch = "x86_64" + return mock_tools + + +def test_list_available_system_images(mock_tools, android_sdk): + """Returns a sorted list of available system image package identifiers.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-34;default;x86_64 | 7 | Intel x86_64 Atom System Image\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " system-images;android-23;default;x86_64 | 3 | Intel x86_64 Atom System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + # android-23 is filtered out (below minimum API level 24) + assert result == [ + "system-images;android-31;default;x86_64", + "system-images;android-34;default;x86_64", + ] + mock_tools.subprocess.check_output.assert_called_once_with( + [android_sdk.sdkmanager_path, "--list"], + env=android_sdk.env, + ) + + +def test_list_available_system_images_custom_min_api_level(mock_tools, android_sdk): + """A custom minimum version filters out images below that version.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-26;default;x86_64 | 3 | Intel x86_64 Atom System Image\n" + " system-images;android-27;default;x86_64 | 3 | Intel x86_64 Atom System Image\n" + " system-images;android-28;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images(min_api_level=28) + + # android-26 and android-27 are filtered out (below custom minimum of 28) + assert result == [ + "system-images;android-28;default;x86_64", + "system-images;android-31;default;x86_64", + ] + + +def test_list_available_system_images_dotted_version(mock_tools, android_sdk): + """System images with dotted versions (e.g. android-36.1) are included.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-36.1;default;x86_64 | 1 | Intel x86_64 Atom System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + assert result == [ + "system-images;android-36.1;default;x86_64", + ] + + +def test_list_available_system_images_dotted_version_below_minimum( + mock_tools, android_sdk +): + """System images with dotted versions below the minimum are filtered out.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-23.1;default;x86_64 | 1 | Intel x86_64 Atom System Image\n" + " system-images;android-23.1;default;arm64-v8a | 1 | arm64 System Image\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " system-images;android-31;default;arm64-v8a | 5 | arm64 System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + # android-23.1 is filtered out (below minimum API level 24) + assert result == [ + "system-images;android-31;default;x86_64", + ] + + +def test_list_available_system_images_named_version(mock_tools, android_sdk): + """System images with named versions (e.g. android-CANARY) are included.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-CANARY;google_apis;x86_64 | 1 | Google APIs Intel x86_64\n" + " system-images;android-CANARY;google_apis;arm64-v8a | 1 | Google APIs arm64\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + assert result == [ + "system-images;android-CANARY;google_apis;x86_64", + ] + + +def test_list_available_system_images_other_abi(mock_tools, android_sdk): + """System images for other architectures are filtered out.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-34;default;x86_64 | 7 | Intel x86_64 Atom System Image\n" + " system-images;android-34;default;arm64-v8a | 7 | arm 64 v8a System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + # Only x86_64 images returned (fixture sets host_arch to x86_64) + assert result == [ + "system-images;android-34;default;x86_64", + ] + + +def test_list_available_system_images_duplicates(mock_tools, android_sdk): + """Duplicate entries in sdkmanager output are deduplicated.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + assert result == [ + "system-images;android-31;default;x86_64", + ] + + +def test_no_available_system_images(mock_tools, android_sdk): + """If no system images are available, an empty list is returned.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + assert result == [] + + +def test_list_available_system_images_failure(mock_tools, android_sdk): + """If sdkmanager fails, an error is raised.""" + mock_tools.subprocess.check_output.side_effect = subprocess.CalledProcessError( + 1, "" + ) + + with pytest.raises(BriefcaseCommandError): + android_sdk.list_available_system_images(min_api_level=ANDROID_MIN_OS_VERSION) + + +def test_list_available_system_images_malformed_package(mock_tools, android_sdk): + """Malformed package entries in sdkmanager output are skipped.""" + mock_tools.subprocess.check_output.return_value = ( + "Available Packages:\n" + " Path | Version | Description\n" + " ------- | ------- | -------\n" + " system-images;android-31;default;x86_64 | 5 | Intel x86_64 Atom System Image\n" + " system-images;android-34 | 7 | Malformed entry\n" + " emulator | 35.4.9 | Android Emulator\n" + ) + + result = android_sdk.list_available_system_images( + min_api_level=ANDROID_MIN_OS_VERSION + ) + + # Malformed entry is skipped + assert result == [ + "system-images;android-31;default;x86_64", + ] diff --git a/tests/integrations/android_sdk/AndroidSDK/test_min_api_level.py b/tests/integrations/android_sdk/AndroidSDK/test_min_api_level.py new file mode 100644 index 000000000..486c979a0 --- /dev/null +++ b/tests/integrations/android_sdk/AndroidSDK/test_min_api_level.py @@ -0,0 +1,25 @@ +from unittest.mock import MagicMock + +from briefcase.integrations.android_sdk import ANDROID_MIN_OS_VERSION, min_api_level + + +def test_min_api_level_int(): + """min_api_level returns the app's min_os_version when configured as an int.""" + app = MagicMock() + app.min_os_version = 28 + assert min_api_level(app) == 28 + + +def test_min_api_level_str(): + """min_api_level coerces min_os_version to int when configured as a string.""" + app = MagicMock() + app.min_os_version = "28" + assert min_api_level(app) == 28 + + +def test_min_api_level_default(): + """min_api_level falls back to ANDROID_MIN_OS_VERSION when min_os_version is not + set.""" + app = MagicMock() + del app.min_os_version + assert min_api_level(app) == ANDROID_MIN_OS_VERSION diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py index 359770c45..ba24c947a 100644 --- a/tests/platforms/android/gradle/test_run.py +++ b/tests/platforms/android/gradle/test_run.py @@ -616,7 +616,9 @@ def test_run_created_emulator(run_command, first_app_config): run_command.run_app(first_app_config, passthrough=[]) # A new emulator was created - run_command.tools.android_sdk.create_emulator.assert_called_once_with() + run_command.tools.android_sdk.create_emulator.assert_called_once_with( + first_app_config + ) # No attempt was made to verify the AVD (it is pre-verified through # the creation process) @@ -935,7 +937,9 @@ def test_run_test_mode_created_emulator(run_command, first_app_config): ) # A new emulator was created - run_command.tools.android_sdk.create_emulator.assert_called_once_with() + run_command.tools.android_sdk.create_emulator.assert_called_once_with( + first_app_config + ) # No attempt was made to verify the AVD (it is pre-verified through # the creation process)