Example #1
0
    def run(self, *arguments):
        """
        Run a command on a device using Android debug bridge, `adb`. The device
        name is mandatory to ensure clarity in the case of multiple attached
        devices.

        :param arguments: List of strings to pass to `adb` as arguments.

        Returns bytes of `adb` output on success; raises an exception on failure.
        """
        # The ADB integration operates on the basis of running commands before
        # checking that they are valid, then parsing output to notice errors.
        # This keeps performance good in the success case.
        try:
            # Capture `stderr` so that if the process exits with failure, the
            # stderr data is in `e.output`.
            return self.command.subprocess.check_output(
                [
                    str(self.android_sdk.adb_path),
                    "-s",
                    self.device,
                ] + list(arguments),
                universal_newlines=True,
                stderr=subprocess.STDOUT,
            )
        except subprocess.CalledProcessError as e:
            if any((DEVICE_NOT_FOUND.match(line)
                    for line in e.output.split("\n"))):
                raise InvalidDeviceError("device id", self.device)
            raise
Example #2
0
def test_invalid_device(mock_sdk):
    """If the device doesn't exist, the error is caught."""
    # Use real `adb` output from launching an activity that does not exist.
    # Mock out the run command on an adb instance
    adb = ADB(mock_sdk, "exampleDevice")
    adb.run = MagicMock(
        side_effect=InvalidDeviceError("device", "exampleDevice"))

    with pytest.raises(InvalidDeviceError):
        adb.clear_log()
Example #3
0
def test_invalid_device(mock_sdk):
    "If the device doesn't exist, the error is caught."
    # Use real `adb` output from launching an activity that does not exist.
    # Mock out the run command on an adb instance
    adb = ADB(mock_sdk, "exampleDevice")
    adb.run = MagicMock(
        side_effect=InvalidDeviceError('device', 'exampleDevice'))

    with pytest.raises(InvalidDeviceError):
        adb.start_app("com.example.sample.package",
                      "com.example.sample.activity")
Example #4
0
def test_invalid_device(mock_sdk, capsys):
    """If the device ID is invalid, an error is raised."""
    # Mock out the adb response for an emulator
    adb = ADB(mock_sdk, "not-a-device")
    adb.run = MagicMock(side_effect=InvalidDeviceError("device", "exampleDevice"))

    # Invoke avd_name
    with pytest.raises(BriefcaseCommandError):
        adb.has_booted()

    # Validate call parameters.
    adb.run.assert_called_once_with("shell", "getprop", "sys.boot_completed")
Example #5
0
def test_invalid_device(mock_sdk, capsys):
    "Invoking `avd_name()` on an invalid device raises an error."
    # Mock out the run command on an adb instance
    adb = ADB(mock_sdk, "exampleDevice")
    adb.run = MagicMock(
        side_effect=InvalidDeviceError('device', 'exampleDevice'))

    # Invoke install
    with pytest.raises(InvalidDeviceError):
        adb.avd_name()

    # Validate call parameters.
    adb.run.assert_called_once_with("emu", "avd", "name")
Example #6
0
def test_invalid_device(mock_sdk, capsys):
    """Invoking `install_apk()` on an invalid device raises an error."""
    # Mock out the run command on an adb instance
    adb = ADB(mock_sdk, "exampleDevice")
    adb.run = MagicMock(
        side_effect=InvalidDeviceError("device", "exampleDevice"))

    # Invoke install
    with pytest.raises(InvalidDeviceError):
        adb.install_apk("example.apk")

    # Validate call parameters.
    adb.run.assert_called_once_with("install", "example.apk")
def test_invalid_device(mock_sdk, capsys):
    "Invoking `force_stop_app()` on an invalid device raises an error."
    # Mock out the run command on an adb instance
    adb = ADB(mock_sdk, "exampleDevice")
    adb.run = MagicMock(side_effect=InvalidDeviceError('device', 'exampleDevice'))

    # Invoke force_stop_app
    with pytest.raises(InvalidDeviceError):
        adb.force_stop_app("com.example.sample.package")

    # Validate call parameters.
    adb.run.assert_called_once_with(
        "shell", "am", "force-stop", "com.example.sample.package"
    )
Example #8
0
    def start_emulator(self, avd):
        """Start an existing Android emulator.

        Returns when the emulator is booted and ready to accept apps.

        :param avd: The AVD of the device.
        """
        if avd in set(self.emulators()):
            print("Starting emulator {avd}...".format(avd=avd))
            emulator_popen = self.command.subprocess.Popen(
                [str(self.emulator_path), '@' + avd, '-dns-server', '8.8.8.8'],
                env=self.env,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )

            # The boot process happens in 2 phases.
            # First, the emulator appears in the device list. However, it's
            # not ready until the boot process has finished. To determine
            # the boot status, we need the device ID, and an ADB connection.

            # Step 1: Wait for the device to appear so we can get an
            # ADB instance for the new device.
            print()
            print('Waiting for emulator to start...', flush=True, end='')
            adb = None
            known_devices = set()
            while adb is None:
                print('.', flush=True, end='')
                if emulator_popen.poll() is not None:
                    raise BriefcaseCommandError("""
Android emulator was unable to start!

Try starting the emulator manually by running:

    {cmdline}

Resolve any problems you discover, then try running your app again. You may
find this page helpful in diagnosing emulator problems.

    https://developer.android.com/studio/run/emulator-acceleration#accel-vm
""".format(cmdline=' '.join(str(arg) for arg in emulator_popen.args)))

                for device, details in sorted(self.devices().items()):
                    # Only process authorized devices that we haven't seen.
                    if details['authorized'] and device not in known_devices:
                        adb = self.adb(device)
                        device_avd = adb.avd_name()

                        if device_avd == avd:
                            # Found an active device that matches
                            # the AVD we are starting.
                            full_name = "@{avd} (running emulator)".format(
                                avd=avd, )
                            break
                        else:
                            # Not the one. Zathras knows.
                            adb = None
                            known_devices.add(device)

                # Try again in 2 seconds...
                self.sleep(2)

            # Print a marker so we can see the phase change
            print(' booting...', flush=True, end='')

            # Phase 2: Wait for the boot process to complete
            while not adb.has_booted():
                if emulator_popen.poll() is not None:
                    raise BriefcaseCommandError("""
Android emulator was unable to boot!

Try starting the emulator manually by running:

    {cmdline}

Resolve any problems you discover, then try running your app again. You may
find this page helpful in diagnosing emulator problems.

    https://developer.android.com/studio/run/emulator-acceleration#accel-vm
""".format(cmdline=' '.join(str(arg) for arg in emulator_popen.args)))

                # Try again in 2 seconds...
                self.sleep(2)
                print('.', flush=True, end='')

            print()
            # Return the device ID and full name.
            return device, full_name
        else:
            raise InvalidDeviceError("emulator AVD", avd)
Example #9
0
    def select_target_device(self, device_or_avd):
        """
        Select a device to be the target for actions.

        Interrogates the system to get the list of available devices.

        If the user has specified a device at the command line, that device
        will be validated, and then automatically selected.

        :param device_or_avd: The device or AVD to target. Can be a physical
            device id (a hex string), an emulator id ("emulator-5554"), or
            an emulator AVD name ("@robotfriend"). If ``None``, the user will
            be asked to select a device from the list available.
        :returns: A tuple containing ``(device, name, avd)``. ``avd``
            will only be provided if an emulator with that AVD is not currently
            running. If ``device`` is null, a new emulator should be created.
        """
        # Get the list of attached devices (includes running emulators)
        running_devices = self.devices()

        # Choices is an ordered list of options that can be shown to the user.
        # Each device should appear only once, and be keyed by AVD only if
        # a device ID isn't available.
        choices = []
        # Device choices is the full lookup list. Devices can be looked up
        # by any valid key - ID *or* AVD.
        device_choices = {}

        # Iterate over all the running devices.
        # If the device is a virtual device, use ADB to get the emulator AVD name.
        # If it is a physical device, use the device name.
        # Keep a log of all running AVDs
        running_avds = {}
        for d, details in sorted(running_devices.items(),
                                 key=lambda d: d[1]["name"]):
            name = details["name"]
            avd = self.adb(d).avd_name()
            if avd:
                # It's a running emulator
                running_avds[avd] = d
                full_name = "@{avd} (running emulator)".format(avd=avd, )
                choices.append((d, full_name))

                # Save the AVD as a device detail.
                details["avd"] = avd

                # Device can be looked up by device ID or AVD
                device_choices[d] = full_name
                device_choices["@" + avd] = full_name
            else:
                # It's a physical device (might be disabled)
                full_name = "{name} ({d})".format(name=name, d=d)
                choices.append((d, full_name))
                device_choices[d] = full_name

        # Add any non-running emulator AVDs to the list of candidate devices
        for avd in self.emulators():
            if avd not in running_avds:
                name = "@{avd} (emulator)".format(avd=avd)
                choices.append(("@" + avd, name))
                device_choices["@" + avd] = name

        # If a device or AVD has been provided, check it against the available
        # device list.
        if device_or_avd:
            try:
                name = device_choices[device_or_avd]

                if device_or_avd.startswith("@"):
                    # specifier is an AVD
                    try:
                        avd = device_or_avd[1:]
                        device = running_avds[avd]
                    except KeyError:
                        # device_or_avd isn't in the list of running avds;
                        # it must be a non-running emulator.
                        return None, name, avd
                else:
                    # Specifier is a direct device ID
                    avd = None
                    device = device_or_avd

                details = running_devices[device]
                avd = details.get("avd")
                if details["authorized"]:
                    # An authorized, running device (emulator or physical)
                    return device, name, avd
                else:
                    # An unauthorized physical device
                    raise AndroidDeviceNotAuthorized(device)

            except KeyError:
                # Provided device_or_id isn't a valid device identifier.
                if device_or_avd.startswith("@"):
                    id_type = "emulator AVD"
                else:
                    id_type = "device ID"
                raise InvalidDeviceError(id_type, device_or_avd)

        # We weren't given a device/AVD; we have to select from the list.
        # If we're selecting from a list, there's always one last choice
        choices.append((None, "Create a new Android emulator"))

        # Show the choices to the user.
        print()
        print("Select device:")
        print()
        try:
            choice = select_option(choices, input=self.command.input)
        except InputDisabled:
            # If input is disabled, and there's only one actual simulator,
            # select it. If there are no simulators, select "Create simulator"
            if len(choices) <= 2:
                choice = choices[0][0]
            else:
                raise BriefcaseCommandError(
                    "Input has been disabled; can't select a device to target."
                )

        # Proces the user's choice
        if choice is None:
            # Create a new emulator. No device ID or AVD.
            device = None
            avd = None
            name = None
        elif choice.startswith("@"):
            # A non-running emulator. We have an AVD, but no device ID.
            device = None
            name = device_choices[choice]
            avd = choice[1:]
        else:
            # Either a running emulator, or a physical device. Regardless,
            # we need to check if the device is developer enabled
            try:
                details = running_devices[choice]
                if not details["authorized"]:
                    # An unauthorized physical device
                    raise AndroidDeviceNotAuthorized(choice)

                # Return the device ID and name.
                device = choice
                name = device_choices[choice]
                avd = details.get("avd")
            except KeyError:
                raise InvalidDeviceError("device ID", choice)

        if avd:
            print("""
In future, you can specify this device by running:

    briefcase run android -d @{avd}

""".format(avd=avd))
        elif device:
            print("""
In future, you can specify this device by running:

    briefcase run android -d {device}

""".format(device=device))

        return device, name, avd
Example #10
0
    def select_target_device(self, udid_or_device=None):
        """
        Select the target device to use for iOS builds.

        Interrogates the system to get the list of available simulators

        If there is only a single iOS version available, that version
        will be selected automatically.

        If there is only one simulator available, that version will be selected
        automatically.

        If the user has specified a device at the command line, it will be
        used in preference to any

        :param udid_or_device: The device to target. Can be a device UUID, a
            device name ("iPhone 11"), or a device name and OS version
            ("iPhone 11::13.3"). If ``None``, the user will be asked to select
            a device at runtime.
        :returns: A tuple containing the udid, iOS version, and device name
            for the selected device.
        """
        simulators = self.get_simulators(self, 'iOS')

        try:
            # Try to convert to a UDID. If this succeeds, then the argument
            # is a UDID.
            udid = str(UUID(udid_or_device)).upper()
            # User has provided a UDID at the command line; look for it.
            for iOS_version, devices in simulators.items():
                try:
                    device = devices[udid]
                    return udid, iOS_version, device
                except KeyError:
                    # UDID doesn't exist in this iOS version; try another.
                    pass

            # We've iterated through all available iOS versions and
            # found no match; return an error.
            raise InvalidDeviceError('device UDID', udid)

        except (ValueError, TypeError):
            # Provided value wasn't a UDID.
            # It must be a device or device+version
            if udid_or_device and '::' in udid_or_device:
                # A device name::version.
                device, iOS_version = udid_or_device.split('::')
                try:
                    devices = simulators[iOS_version]
                    try:
                        # Do a reverse lookup for UDID, based on a
                        # case-insensitive name lookup.
                        udid = {
                            name.lower(): udid
                            for udid, name in devices.items()
                        }[device.lower()]

                        # Found a match;
                        # normalize back to the official name and return.
                        device = devices[udid]
                        return udid, iOS_version, device
                    except KeyError:
                        raise InvalidDeviceError('device name', device)
                except KeyError:
                    raise InvalidDeviceError('iOS Version', iOS_version)
            elif udid_or_device:
                # Just a device name
                device = udid_or_device

                # Search iOS versions, looking for most recent version first.
                for iOS_version, devices in sorted(
                        simulators.items(),
                        key=lambda item: tuple(
                            int(v) for v in item[0].split('.')),
                        reverse=True):
                    try:
                        udid = {
                            name.lower(): udid
                            for udid, name in devices.items()
                        }[device.lower()]

                        # Found a match;
                        # normalize back to the official name and return.
                        device = devices[udid]
                        return udid, iOS_version, device
                    except KeyError:
                        # UDID doesn't exist in this iOS version; try another.
                        pass
                raise InvalidDeviceError('device name', device)

        if len(simulators) == 0:
            raise BriefcaseCommandError("No iOS simulators available.")
        elif len(simulators) == 1:
            iOS_version = list(simulators.keys())[0]
        else:
            if self.input.enabled:
                print()
                print("Select iOS version:")
                print()
            iOS_version = select_option(
                {version: version
                 for version in simulators.keys()},
                input=self.input)

        devices = simulators[iOS_version]

        if len(devices) == 0:
            raise BriefcaseCommandError(
                "No simulators available for iOS {iOS_version}.".format(
                    iOS_version=iOS_version))
        elif len(devices) == 1:
            udid = list(devices.keys())[0]
        else:
            if self.input.enabled:
                print()
                print("Select simulator device:")
                print()
            udid = select_option(devices, input=self.input)

        device = devices[udid]

        print("In future, you could specify this device by running:")
        print()
        print('    briefcase {self.command} iOS -d "{device}::{iOS_version}"'.
              format(self=self, device=device, iOS_version=iOS_version))
        print()
        print('or:')
        print()
        print("    briefcase {self.command} iOS -d {udid}".format(self=self,
                                                                  udid=udid))
        return udid, iOS_version, device
Example #11
0
    def select_target_device(self, udid_or_device=None):
        """Select the target device to use for iOS builds.

        Interrogates the system to get the list of available simulators

        If there is only a single iOS version available, that version
        will be selected automatically.

        If there is only one simulator available, that version will be selected
        automatically.

        If the user has specified a device at the command line, it will be
        used in preference to any

        :param udid_or_device: The device to target. Can be a device UUID, a
            device name ("iPhone 11"), or a device name and OS version
            ("iPhone 11::13.3"). If ``None``, the user will be asked to select
            a device at runtime.
        :returns: A tuple containing the udid, iOS version, and device name
            for the selected device.
        """
        simulators = self.get_simulators(self, "iOS")

        try:
            # Try to convert to a UDID. If this succeeds, then the argument
            # is a UDID.
            udid = str(UUID(udid_or_device)).upper()
            # User has provided a UDID at the command line; look for it.
            for iOS_version, devices in simulators.items():
                try:
                    device = devices[udid]
                    return udid, iOS_version, device
                except KeyError:
                    # UDID doesn't exist in this iOS version; try another.
                    pass

            # We've iterated through all available iOS versions and
            # found no match; return an error.
            raise InvalidDeviceError("device UDID", udid)

        except (ValueError, TypeError) as e:
            # Provided value wasn't a UDID.
            # It must be a device or device+version
            if udid_or_device and "::" in udid_or_device:
                # A device name::version.
                device, iOS_version = udid_or_device.split("::")
                try:
                    # Convert the simulator dict into a dict where
                    # the iOS versions are lower cased, then do a lookup
                    # on the lower case name provided by the user.
                    # However, also return the *unmodified* iOS version string
                    # so we can convert the user-provided iOS version into the
                    # "clean", official capitalization.
                    iOS_version, devices = {
                        clean_iOS_version.lower(): (clean_iOS_version, details)
                        for clean_iOS_version, details in simulators.items()
                    }[iOS_version.lower()]
                    try:
                        # Do a reverse lookup for UDID, based on a
                        # case-insensitive name lookup.
                        udid = {name.lower(): udid for udid, name in devices.items()}[
                            device.lower()
                        ]

                        # Found a match;
                        # normalize back to the official name and return.
                        device = devices[udid]
                        return udid, iOS_version, device
                    except KeyError as e:
                        raise InvalidDeviceError("device name", device) from e
                except KeyError as err:
                    raise InvalidDeviceError("iOS Version", iOS_version) from err
            elif udid_or_device:
                # Just a device name
                device = udid_or_device

                # Search iOS versions, looking for most recent version first.
                # The iOS version string will be something like "iOS 15.4";
                # Drop the prefix (if it exists), convert into the tuple (15, 4),
                # and sort numerically.
                for iOS_version, devices in sorted(
                    simulators.items(),
                    key=lambda item: tuple(
                        int(v) for v in item[0].split()[-1].split(".")
                    ),
                    reverse=True,
                ):
                    try:
                        udid = {name.lower(): udid for udid, name in devices.items()}[
                            device.lower()
                        ]

                        # Found a match;
                        # normalize back to the official name and return.
                        device = devices[udid]
                        return udid, iOS_version, device
                    except KeyError:
                        # UDID doesn't exist in this iOS version; try another.
                        pass
                raise InvalidDeviceError("device name", device) from e

        if len(simulators) == 0:
            raise BriefcaseCommandError("No iOS simulators available.")
        elif len(simulators) == 1:
            iOS_version = list(simulators.keys())[0]
        else:
            self.input.prompt()
            self.input.prompt("Select iOS version:")
            self.input.prompt()
            iOS_version = select_option(
                {version: version for version in simulators.keys()}, input=self.input
            )

        devices = simulators[iOS_version]

        if len(devices) == 0:
            raise BriefcaseCommandError(f"No simulators available for {iOS_version}.")
        elif len(devices) == 1:
            udid = list(devices.keys())[0]
        else:
            self.input.prompt()
            self.input.prompt("Select simulator device:")
            self.input.prompt()
            udid = select_option(devices, input=self.input)

        device = devices[udid]

        self.logger.info(
            f"""
In the future, you could specify this device by running:

    briefcase {self.command} iOS -d "{device}::{iOS_version}"

or:

    briefcase {self.command} iOS -d {udid}
"""
        )
        return udid, iOS_version, device