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
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()
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")
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")
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")
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" )
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)
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
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
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