Update device selection to wait for wireless devices to load (#122932)

Update device selection to wait for wireless devices to load
This commit is contained in:
Victoria Ashworth 2023-03-29 12:58:07 -05:00 committed by GitHub
parent 820ec70a8d
commit fa01649a59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 3808 additions and 366 deletions

View file

@ -216,6 +216,7 @@ abstract class Logger {
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
});
/// Send an event to be emitted.
@ -376,11 +377,13 @@ class DelegatingLogger implements Logger {
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
}) {
return _delegate.startSpinner(
onFinish: onFinish,
timeout: timeout,
slowWarningCallback: slowWarningCallback,
warningColor: warningColor,
);
}
@ -587,6 +590,7 @@ class StdoutLogger extends Logger {
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
}) {
if (_status != null || !supportsColor) {
return SilentStatus(
@ -606,6 +610,7 @@ class StdoutLogger extends Logger {
terminal: terminal,
timeout: timeout,
slowWarningCallback: slowWarningCallback,
warningColor: warningColor,
)..start();
return _status!;
}
@ -888,6 +893,7 @@ class BufferLogger extends Logger {
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
}) {
return SilentStatus(
stopwatch: _stopwatchFactory.createStopwatch(),
@ -1269,6 +1275,7 @@ class AnonymousSpinnerStatus extends Status {
required Stdio stdio,
required Terminal terminal,
this.slowWarningCallback,
this.warningColor,
super.timeout,
}) : _stdio = stdio,
_terminal = terminal,
@ -1278,6 +1285,7 @@ class AnonymousSpinnerStatus extends Status {
final Terminal _terminal;
String _slowWarning = '';
final SlowWarningCallback? slowWarningCallback;
final TerminalColor? warningColor;
static const String _backspaceChar = '\b';
static const String _clearChar = ' ';
@ -1360,8 +1368,15 @@ class AnonymousSpinnerStatus extends Status {
_clear(_currentLineLength - _lastAnimationFrameLength);
}
}
if (_slowWarning == '' && slowWarningCallback != null) {
_slowWarning = slowWarningCallback!();
final SlowWarningCallback? callback = slowWarningCallback;
if (_slowWarning.isEmpty && callback != null) {
final TerminalColor? color = warningColor;
if (color != null) {
_slowWarning = _terminal.color(callback(), color);
} else {
_slowWarning = callback();
}
_writeToStdOut(_slowWarning);
}
}

View file

@ -175,6 +175,15 @@ class AnsiTerminal implements Terminal {
static const String yellow = '\u001b[33m';
static const String grey = '\u001b[90m';
// Moves cursor up 1 line.
static const String cursorUpLineCode = '\u001b[1A';
// Moves cursor to the beginning of the line.
static const String cursorBeginningOfLineCode = '\u001b[1G';
// Clear the entire line, cursor position does not change.
static const String clearEntireLineCode = '\u001b[2K';
static const Map<TerminalColor, String> _colorMap = <TerminalColor, String>{
TerminalColor.red: red,
TerminalColor.green: green,
@ -268,6 +277,19 @@ class AnsiTerminal implements Terminal {
@override
String clearScreen() => supportsColor ? clear : '\n\n';
/// Returns ANSI codes to clear [numberOfLines] lines starting with the line
/// the cursor is on.
///
/// If the terminal does not support ANSI codes, returns an empty string.
String clearLines(int numberOfLines) {
if (!supportsColor) {
return '';
}
return cursorBeginningOfLineCode +
clearEntireLineCode +
(cursorUpLineCode + clearEntireLineCode) * (numberOfLines - 1);
}
@override
bool get singleCharMode {
if (!_stdio.stdinHasTerminal) {

View file

@ -272,7 +272,6 @@ class UserMessages {
String get flutterFoundButUnsupportedDevices => 'The following devices were found, but are not supported by this project:';
String flutterFoundSpecifiedDevices(int count, String deviceId) =>
'Found $count devices with name or id matching $deviceId:';
String get flutterMultipleDevicesFound => 'Multiple devices found:';
String flutterChooseDevice(int option, String name, String deviceId) => '[$option]: $name ($deviceId)';
String get flutterChooseOne => 'Please choose one (or "q" to quit)';
String get flutterSpecifyDeviceWithAllOption =>

View file

@ -175,6 +175,9 @@ known, it can be explicitly provided to attach via the command-line, e.g.
@override
final String category = FlutterCommandCategory.tools;
@override
bool get refreshWirelessDevices => true;
int? get debugPort {
if (argResults!['debug-port'] == null) {
return null;

View file

@ -878,12 +878,12 @@ class DeviceDomain extends Domain {
final List<PollingDeviceDiscovery> _discoverers = <PollingDeviceDiscovery>[];
/// Return a list of the current devices, with each device represented as a map
/// of properties (id, name, platform, ...).
/// Return a list of the currently connected devices, with each device
/// represented as a map of properties (id, name, platform, ...).
Future<List<Map<String, Object?>>> getDevices([ Map<String, Object?>? args ]) async {
return <Map<String, Object?>>[
for (final PollingDeviceDiscovery discoverer in _discoverers)
for (final Device device in await discoverer.devices())
for (final Device device in await discoverer.devices(filter: DeviceDiscoveryFilter()))
await _deviceToMap(device),
];
}
@ -1066,10 +1066,12 @@ class DeviceDomain extends Domain {
return Future<void>.value();
}
/// Return the device matching the deviceId field in the args.
/// Return the connected device matching the deviceId field in the args.
Future<Device?> _getDevice(String? deviceId) async {
for (final PollingDeviceDiscovery discoverer in _discoverers) {
final List<Device> devices = await discoverer.devices();
final List<Device> devices = await discoverer.devices(
filter: DeviceDiscoveryFilter(),
);
Device? device;
for (final Device localDevice in devices) {
if (localDevice.id == deviceId) {

View file

@ -3,6 +3,9 @@
// found in the LICENSE file.
import '../base/common.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../convert.dart';
import '../device.dart';
@ -63,6 +66,9 @@ class DevicesCommand extends FlutterCommand {
}
final DevicesCommandOutput output = DevicesCommandOutput(
platform: globals.platform,
logger: globals.logger,
deviceManager: globals.deviceManager,
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
);
@ -75,8 +81,35 @@ class DevicesCommand extends FlutterCommand {
}
class DevicesCommandOutput {
DevicesCommandOutput({this.deviceDiscoveryTimeout});
factory DevicesCommandOutput({
required Platform platform,
required Logger logger,
DeviceManager? deviceManager,
Duration? deviceDiscoveryTimeout,
}) {
if (platform.isMacOS) {
return DevicesCommandOutputWithExtendedWirelessDeviceDiscovery(
logger: logger,
deviceManager: deviceManager,
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
);
}
return DevicesCommandOutput._private(
logger: logger,
deviceManager: deviceManager,
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
);
}
DevicesCommandOutput._private({
required Logger logger,
required DeviceManager? deviceManager,
required this.deviceDiscoveryTimeout,
}) : _deviceManager = deviceManager,
_logger = logger;
final DeviceManager? _deviceManager;
final Logger _logger;
final Duration? deviceDiscoveryTimeout;
Future<List<Device>> _getAttachedDevices(DeviceManager deviceManager) async {
@ -98,7 +131,7 @@ class DevicesCommandOutput {
Future<void> findAndOutputAllTargetDevices({required bool machine}) async {
List<Device> attachedDevices = <Device>[];
List<Device> wirelessDevices = <Device>[];
final DeviceManager? deviceManager = globals.deviceManager;
final DeviceManager? deviceManager = _deviceManager;
if (deviceManager != null) {
// Refresh the cache and then get the attached and wireless devices from
// the cache.
@ -117,15 +150,15 @@ class DevicesCommandOutput {
_printNoDevicesDetected();
} else {
if (attachedDevices.isNotEmpty) {
globals.printStatus('${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n');
await Device.printDevices(attachedDevices, globals.logger);
_logger.printStatus('${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n');
await Device.printDevices(attachedDevices, _logger);
}
if (wirelessDevices.isNotEmpty) {
if (attachedDevices.isNotEmpty) {
globals.printStatus('');
_logger.printStatus('');
}
globals.printStatus('${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:\n');
await Device.printDevices(wirelessDevices, globals.logger);
_logger.printStatus('${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:\n');
await Device.printDevices(wirelessDevices, _logger);
}
}
await _printDiagnostics();
@ -143,24 +176,125 @@ class DevicesCommandOutput {
}
status.write('Visit https://flutter.dev/setup/ for troubleshooting tips.');
globals.printStatus(status.toString());
_logger.printStatus(status.toString());
}
Future<void> _printDiagnostics() async {
final List<String> diagnostics = await globals.deviceManager?.getDeviceDiagnostics() ?? <String>[];
final List<String> diagnostics = await _deviceManager?.getDeviceDiagnostics() ?? <String>[];
if (diagnostics.isNotEmpty) {
globals.printStatus('');
_logger.printStatus('');
for (final String diagnostic in diagnostics) {
globals.printStatus('$diagnostic', hangingIndent: 2);
_logger.printStatus('$diagnostic', hangingIndent: 2);
}
}
}
Future<void> printDevicesAsJson(List<Device> devices) async {
globals.printStatus(
_logger.printStatus(
const JsonEncoder.withIndent(' ').convert(
await Future.wait(devices.map((Device d) => d.toJson()))
)
);
}
}
const String _checkingForWirelessDevicesMessage = 'Checking for wireless devices...';
const String _noAttachedCheckForWireless = 'No devices found yet. Checking for wireless devices...';
const String _noWirelessDevicesFoundMessage = 'No wireless devices were found.';
class DevicesCommandOutputWithExtendedWirelessDeviceDiscovery extends DevicesCommandOutput {
DevicesCommandOutputWithExtendedWirelessDeviceDiscovery({
required super.logger,
super.deviceManager,
super.deviceDiscoveryTimeout,
}) : super._private();
@override
Future<void> findAndOutputAllTargetDevices({required bool machine}) async {
// When a user defines the timeout, use the super function that does not do
// longer wireless device discovery.
if (deviceDiscoveryTimeout != null) {
return super.findAndOutputAllTargetDevices(machine: machine);
}
if (machine) {
final List<Device> devices = await _deviceManager?.refreshAllDevices(
filter: DeviceDiscoveryFilter(),
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
) ?? <Device>[];
await printDevicesAsJson(devices);
return;
}
final Future<void>? extendedWirelessDiscovery = _deviceManager?.refreshExtendedWirelessDeviceDiscoverers(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
);
List<Device> attachedDevices = <Device>[];
final DeviceManager? deviceManager = _deviceManager;
if (deviceManager != null) {
attachedDevices = await _getAttachedDevices(deviceManager);
}
// Number of lines to clear starts at 1 because it's inclusive of the line
// the cursor is on, which will be blank for this use case.
int numLinesToClear = 1;
// Display list of attached devices.
if (attachedDevices.isNotEmpty) {
_logger.printStatus('${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n');
await Device.printDevices(attachedDevices, _logger);
_logger.printStatus('');
numLinesToClear += 1;
}
// Display waiting message.
if (attachedDevices.isEmpty) {
_logger.printStatus(_noAttachedCheckForWireless);
} else {
_logger.printStatus(_checkingForWirelessDevicesMessage);
}
numLinesToClear += 1;
final Status waitingStatus = _logger.startSpinner();
await extendedWirelessDiscovery;
List<Device> wirelessDevices = <Device>[];
if (deviceManager != null) {
wirelessDevices = await _getWirelessDevices(deviceManager);
}
waitingStatus.stop();
final Terminal terminal = _logger.terminal;
if (_logger.isVerbose) {
// Reprint the attach devices.
if (attachedDevices.isNotEmpty) {
_logger.printStatus('\n${attachedDevices.length} connected ${pluralize('device', attachedDevices.length)}:\n');
await Device.printDevices(attachedDevices, _logger);
}
} else if (terminal.supportsColor && terminal is AnsiTerminal) {
_logger.printStatus(
terminal.clearLines(numLinesToClear),
newline: false,
);
}
if (attachedDevices.isNotEmpty || !_logger.terminal.supportsColor) {
_logger.printStatus('');
}
if (wirelessDevices.isEmpty) {
if (attachedDevices.isEmpty) {
// No wireless or attached devices were found.
_printNoDevicesDetected();
} else {
// Attached devices found, wireless devices not found.
_logger.printStatus(_noWirelessDevicesFoundMessage);
}
} else {
// Display list of wireless devices.
_logger.printStatus('${wirelessDevices.length} wirelessly connected ${pluralize('device', wirelessDevices.length)}:\n');
await Device.printDevices(wirelessDevices, _logger);
}
await _printDiagnostics();
}
}

View file

@ -35,6 +35,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
@override
final String category = FlutterCommandCategory.tools;
@override
bool get refreshWirelessDevices => true;
Device? device;
bool get uninstallOnly => boolArg('uninstall-only');

View file

@ -30,6 +30,9 @@ class LogsCommand extends FlutterCommand {
@override
final String category = FlutterCommandCategory.tools;
@override
bool get refreshWirelessDevices => true;
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{};

View file

@ -199,6 +199,9 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
bool get uninstallFirst => boolArg('uninstall-first');
bool get enableEmbedderApi => boolArg('enable-embedder-api');
@override
bool get refreshWirelessDevices => true;
@override
bool get reportNullSafety => true;

View file

@ -65,6 +65,9 @@ class ScreenshotCommand extends FlutterCommand {
@override
final String category = FlutterCommandCategory.tools;
@override
bool get refreshWirelessDevices => true;
@override
final List<String> aliases = <String>['pic'];

View file

@ -102,6 +102,11 @@ abstract class DeviceManager {
_specifiedDeviceId = id;
}
/// A minimum duration to use when discovering wireless iOS devices.
static const Duration minimumWirelessDeviceDiscoveryTimeout = Duration(
seconds: 5,
);
/// True when the user has specified a single specific device.
bool get hasSpecifiedDeviceId => specifiedDeviceId != null;
@ -231,6 +236,22 @@ abstract class DeviceManager {
return devices.expand<Device>((List<Device> deviceList) => deviceList).toList();
}
/// Discard existing cache of discoverers that are known to take longer to
/// discover wireless devices.
///
/// Then, search for devices for those discoverers to populate the cache for
/// no longer than [timeout].
Future<void> refreshExtendedWirelessDeviceDiscoverers({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async {
await Future.wait<List<Device>>(<Future<List<Device>>>[
for (final DeviceDiscovery discoverer in _platformDiscoverers)
if (discoverer.requiresExtendedWirelessDeviceDiscovery)
discoverer.discoverDevices(timeout: timeout)
]);
}
/// Whether we're capable of listing any devices given the current environment configuration.
bool get canListAnything {
return _platformDiscoverers.any((DeviceDiscovery discoverer) => discoverer.canListAnything);
@ -434,6 +455,10 @@ abstract class DeviceDiscovery {
/// current environment configuration.
bool get canListAnything;
/// Whether this device discovery is known to take longer to discover
/// wireless devices.
bool get requiresExtendedWirelessDeviceDiscovery => false;
/// Return all connected devices, cached on subsequent calls.
Future<List<Device>> devices({DeviceDiscoveryFilter? filter});
@ -504,6 +529,8 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
/// Get devices from cache filtered by [filter].
///
/// If the cache is empty, populate the cache.
///
/// If [filter] is null, it may return devices that are not connected.
@override
Future<List<Device>> devices({DeviceDiscoveryFilter? filter}) {
return _populateDevices(filter: filter);
@ -512,6 +539,8 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery {
/// Empty the cache and repopulate it before getting devices from cache filtered by [filter].
///
/// Search for devices to populate the cache for no longer than [timeout].
///
/// If [filter] is null, it may return devices that are not connected.
@override
Future<List<Device>> discoverDevices({
Duration? timeout,

View file

@ -680,7 +680,9 @@ class DeviceValidator extends DoctorValidator {
@override
Future<ValidationResult> validate() async {
final List<Device> devices = await _deviceManager.getAllDevices();
final List<Device> devices = await _deviceManager.refreshAllDevices(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
);
List<ValidationMessage> installedMessages = <ValidationMessage>[];
if (devices.isNotEmpty) {
installedMessages = (await Device.descriptions(devices))

View file

@ -35,26 +35,30 @@ import 'mac.dart';
class IOSDevices extends PollingDeviceDiscovery {
IOSDevices({
required Platform platform,
required XCDevice xcdevice,
required this.xcdevice,
required IOSWorkflow iosWorkflow,
required Logger logger,
}) : _platform = platform,
_xcdevice = xcdevice,
_iosWorkflow = iosWorkflow,
_logger = logger,
super('iOS devices');
final Platform _platform;
final XCDevice _xcdevice;
final IOSWorkflow _iosWorkflow;
final Logger _logger;
@visibleForTesting
final XCDevice xcdevice;
@override
bool get supportsPlatform => _platform.isMacOS;
@override
bool get canListAnything => _iosWorkflow.canListDevices;
@override
bool get requiresExtendedWirelessDeviceDiscovery => true;
StreamSubscription<Map<XCDeviceEvent, String>>? _observedDeviceEventsSubscription;
@override
@ -64,18 +68,22 @@ class IOSDevices extends PollingDeviceDiscovery {
'Control of iOS devices or simulators only supported on macOS.'
);
}
if (!_xcdevice.isInstalled) {
if (!xcdevice.isInstalled) {
return;
}
deviceNotifier ??= ItemListNotifier<Device>();
// Start by populating all currently attached devices.
deviceNotifier!.updateWithNewList(await pollingGetDevices());
final List<Device> devices = await pollingGetDevices();
// Only show connected devices.
final List<Device> filteredDevices = devices.where((Device device) => device.isConnected == true).toList();
deviceNotifier!.updateWithNewList(filteredDevices);
// cancel any outstanding subscriptions.
await _observedDeviceEventsSubscription?.cancel();
_observedDeviceEventsSubscription = _xcdevice.observedDeviceEvents()?.listen(
_observedDeviceEventsSubscription = xcdevice.observedDeviceEvents()?.listen(
_onDeviceEvent,
onError: (Object error, StackTrace stack) {
_logger.printTrace('Process exception running xcdevice observe:\n$error\n$stack');
@ -109,7 +117,10 @@ class IOSDevices extends PollingDeviceDiscovery {
// There's no way to get details for an individual attached device,
// so repopulate them all.
final List<Device> devices = await pollingGetDevices();
notifier.updateWithNewList(devices);
// Only show connected devices.
final List<Device> filteredDevices = devices.where((Device device) => device.isConnected == true).toList();
notifier.updateWithNewList(filteredDevices);
} else if (eventType == XCDeviceEvent.detach && knownDevice != null) {
notifier.removeItem(knownDevice);
}
@ -128,7 +139,26 @@ class IOSDevices extends PollingDeviceDiscovery {
);
}
return _xcdevice.getAvailableIOSDevices(timeout: timeout);
return xcdevice.getAvailableIOSDevices(timeout: timeout);
}
Future<Device?> waitForDeviceToConnect(
IOSDevice device,
Logger logger,
) async {
final XCDeviceEventNotification? eventDetails =
await xcdevice.waitForDeviceToConnect(device.id);
if (eventDetails != null) {
device.isConnected = true;
device.connectionInterface = eventDetails.eventInterface.connectionInterface;
return device;
}
return null;
}
void cancelWaitForDeviceToConnect() {
xcdevice.cancelWaitForDeviceToConnect();
}
@override
@ -139,7 +169,7 @@ class IOSDevices extends PollingDeviceDiscovery {
];
}
return _xcdevice.getDiagnostics();
return xcdevice.getDiagnostics();
}
@override
@ -152,6 +182,7 @@ class IOSDevice extends Device {
required this.name,
required this.cpuArchitecture,
required this.connectionInterface,
required this.isConnected,
String? sdkVersion,
required Platform platform,
required IOSDeploy iosDeploy,
@ -200,7 +231,15 @@ class IOSDevice extends Device {
final DarwinArch cpuArchitecture;
@override
final DeviceConnectionInterface connectionInterface;
/// The [connectionInterface] provided from `XCDevice.getAvailableIOSDevices`
/// may not be accurate. Sometimes if it doesn't have a long enough time
/// to connect, wireless devices will have an interface of `usb`/`attached`.
/// This may change after waiting for the device to connect in
/// `waitForDeviceToConnect`.
DeviceConnectionInterface connectionInterface;
@override
bool isConnected;
final Map<IOSApp?, DeviceLogReader> _logReaders = <IOSApp?, DeviceLogReader>{};

View file

@ -4,6 +4,7 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
@ -23,11 +24,36 @@ import '../ios/mac.dart';
import '../reporting/reporting.dart';
import 'xcode.dart';
class XCDeviceEventNotification {
XCDeviceEventNotification(
this.eventType,
this.eventInterface,
this.deviceIdentifier,
);
final XCDeviceEvent eventType;
final XCDeviceEventInterface eventInterface;
final String deviceIdentifier;
}
enum XCDeviceEvent {
attach,
detach,
}
enum XCDeviceEventInterface {
usb(name: 'usb', connectionInterface: DeviceConnectionInterface.attached),
wifi(name: 'wifi', connectionInterface: DeviceConnectionInterface.wireless);
const XCDeviceEventInterface({
required this.name,
required this.connectionInterface,
});
final String name;
final DeviceConnectionInterface connectionInterface;
}
/// A utility class for interacting with Xcode xcdevice command line tools.
class XCDevice {
XCDevice({
@ -61,6 +87,8 @@ class XCDevice {
void dispose() {
_deviceObservationProcess?.kill();
_usbDeviceWaitProcess?.kill();
_wifiDeviceWaitProcess?.kill();
}
final ProcessUtils _processUtils;
@ -74,6 +102,12 @@ class XCDevice {
Process? _deviceObservationProcess;
StreamController<Map<XCDeviceEvent, String>>? _deviceIdentifierByEvent;
@visibleForTesting
StreamController<XCDeviceEventNotification>? waitStreamController;
Process? _usbDeviceWaitProcess;
Process? _wifiDeviceWaitProcess;
void _setupDeviceIdentifierByEventStream() {
// _deviceIdentifierByEvent Should always be available for listeners
// in case polling needs to be stopped and restarted.
@ -172,27 +206,14 @@ class XCDevice {
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
// xcdevice observe example output of UDIDs:
//
// Listening for all devices, on both interfaces.
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: 00008027-00192736010F802E
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
final RegExpMatch? match = _observationIdentifierPattern.firstMatch(line);
if (match != null && match.groupCount == 2) {
final String verb = match.group(1)!.toLowerCase();
final String identifier = match.group(2)!;
if (verb.startsWith('attach')) {
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
XCDeviceEvent.attach: identifier,
});
} else if (verb.startsWith('detach')) {
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
XCDeviceEvent.detach: identifier,
});
}
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
XCDeviceEventInterface.usb,
);
if (event != null) {
_deviceIdentifierByEvent?.add(<XCDeviceEvent, String>{
event.eventType: event.deviceIdentifier,
});
}
});
final StreamSubscription<String> stderrSubscription = _deviceObservationProcess!.stderr
@ -222,10 +243,183 @@ class XCDevice {
}
}
XCDeviceEventNotification? _processXCDeviceStdOut(
String line,
XCDeviceEventInterface eventInterface,
) {
// xcdevice observe example output of UDIDs:
//
// Listening for all devices, on both interfaces.
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: 00008027-00192736010F802E
// Detach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
// Attach: d83d5bc53967baa0ee18626ba87b6254b2ab5418
final RegExpMatch? match = _observationIdentifierPattern.firstMatch(line);
if (match != null && match.groupCount == 2) {
final String verb = match.group(1)!.toLowerCase();
final String identifier = match.group(2)!;
if (verb.startsWith('attach')) {
return XCDeviceEventNotification(
XCDeviceEvent.attach,
eventInterface,
identifier,
);
} else if (verb.startsWith('detach')) {
return XCDeviceEventNotification(
XCDeviceEvent.detach,
eventInterface,
identifier,
);
}
}
return null;
}
void _stopObservingTetheredIOSDevices() {
_deviceObservationProcess?.kill();
}
/// Wait for a connect event for a specific device. Must use device's exact UDID.
///
/// To cancel this process, call [cancelWaitForDeviceToConnect].
Future<XCDeviceEventNotification?> waitForDeviceToConnect(
String deviceId,
) async {
try {
if (_usbDeviceWaitProcess != null || _wifiDeviceWaitProcess != null) {
throw Exception('xcdevice wait restart failed');
}
waitStreamController = StreamController<XCDeviceEventNotification>();
// Run in interactive mode (via script) to convince
// xcdevice it has a terminal attached in order to redirect stdout.
_usbDeviceWaitProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'wait',
'--${XCDeviceEventInterface.usb.name}',
deviceId,
],
);
_wifiDeviceWaitProcess = await _processUtils.start(
<String>[
'script',
'-t',
'0',
'/dev/null',
..._xcode.xcrunCommand(),
'xcdevice',
'wait',
'--${XCDeviceEventInterface.wifi.name}',
deviceId,
],
);
final StreamSubscription<String> usbStdoutSubscription = _processWaitStdOut(
_usbDeviceWaitProcess!,
XCDeviceEventInterface.usb,
);
final StreamSubscription<String> wifiStdoutSubscription = _processWaitStdOut(
_wifiDeviceWaitProcess!,
XCDeviceEventInterface.wifi,
);
final StreamSubscription<String> usbStderrSubscription = _usbDeviceWaitProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice wait --usb error: $line');
});
final StreamSubscription<String> wifiStderrSubscription = _wifiDeviceWaitProcess!.stderr
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
_logger.printTrace('xcdevice wait --wifi error: $line');
});
final Future<void> usbProcessExited = _usbDeviceWaitProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice wait --usb exited with code $exitCode');
// Kill other process in case only one was killed.
_wifiDeviceWaitProcess?.kill();
unawaited(usbStdoutSubscription.cancel());
unawaited(usbStderrSubscription.cancel());
});
final Future<void> wifiProcessExited = _wifiDeviceWaitProcess!.exitCode.then((int status) {
_logger.printTrace('xcdevice wait --wifi exited with code $exitCode');
// Kill other process in case only one was killed.
_usbDeviceWaitProcess?.kill();
unawaited(wifiStdoutSubscription.cancel());
unawaited(wifiStderrSubscription.cancel());
});
final Future<void> allProcessesExited = Future.wait(
<Future<void>>[
usbProcessExited,
wifiProcessExited,
]).whenComplete(() async {
_usbDeviceWaitProcess = null;
_wifiDeviceWaitProcess = null;
await waitStreamController?.close();
});
return await Future.any(
<Future<XCDeviceEventNotification?>>[
allProcessesExited.then((_) => null),
waitStreamController!.stream.first.whenComplete(() async {
cancelWaitForDeviceToConnect();
}),
],
);
} on ProcessException catch (exception, stackTrace) {
_logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
} on ArgumentError catch (exception, stackTrace) {
_logger.printTrace('Process exception running xcdevice wait:\n$exception\n$stackTrace');
} on StateError {
_logger.printTrace('Stream broke before first was found');
return null;
}
return null;
}
StreamSubscription<String> _processWaitStdOut(
Process process,
XCDeviceEventInterface eventInterface,
) {
return process.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen((String line) {
final XCDeviceEventNotification? event = _processXCDeviceStdOut(
line,
eventInterface,
);
if (event != null && event.eventType == XCDeviceEvent.attach) {
waitStreamController?.add(event);
}
});
}
void cancelWaitForDeviceToConnect() {
_usbDeviceWaitProcess?.kill();
_wifiDeviceWaitProcess?.kill();
}
/// A list of [IOSDevice]s. This list includes connected devices and
/// disconnected wireless devices.
///
/// Sometimes devices may have incorrect connection information
/// (`isConnected`, `connectionInterface`) if it timed out before it could get the
/// information. Wireless devices can take longer to get the correct
/// information.
///
/// [timeout] defaults to 2 seconds.
Future<List<IOSDevice>> getAvailableIOSDevices({ Duration? timeout }) async {
final List<Object>? allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2));
@ -284,6 +478,7 @@ class XCDevice {
continue;
}
bool isConnected = true;
final Map<String, Object?>? errorProperties = _errorProperties(device);
if (errorProperties != null) {
final String? errorMessage = _parseErrorMessage(errorProperties);
@ -300,7 +495,7 @@ class XCDevice {
// Sometimes the app launch will fail on these devices until Xcode is done setting up the device.
// Other times this is a false positive and the app will successfully launch despite the error.
if (code != -10) {
continue;
isConnected = false;
}
}
@ -318,6 +513,7 @@ class XCDevice {
name: name,
cpuArchitecture: _cpuArchitecture(device),
connectionInterface: _interfaceType(device),
isConnected: isConnected,
sdkVersion: sdkVersion,
iProxy: _iProxy,
fileSystem: globals.fs,
@ -329,7 +525,6 @@ class XCDevice {
}
}
return devices;
}
/// Despite the name, com.apple.platform.iphoneos includes iPhone, iPads, and all iOS devices.

View file

@ -210,6 +210,11 @@ abstract class FlutterCommand extends Command<void> {
bool get deprecated => false;
/// When the command runs and this is true, trigger an async process to
/// discover devices from discoverers that support wireless devices for an
/// extended amount of time and refresh the device cache with the results.
bool get refreshWirelessDevices => false;
@override
bool get hidden => deprecated;
@ -719,6 +724,7 @@ abstract class FlutterCommand extends Command<void> {
}();
late final TargetDevices _targetDevices = TargetDevices(
platform: globals.platform,
deviceManager: globals.deviceManager!,
logger: globals.logger,
);
@ -1466,6 +1472,14 @@ Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and
}
globals.preRunValidator.validate();
if (refreshWirelessDevices) {
// Loading wireless devices takes longer so start it early.
_targetDevices.startExtendedWirelessDeviceDiscovery(
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
);
}
// Populate the cache. We call this before pub get below so that the
// sky_engine package is available in the flutter cache for pub to find.
if (shouldUpdateCache) {
@ -1656,14 +1670,17 @@ Run 'flutter -h' (or 'flutter <command> -h') for available flutter commands and
}
/// A mixin which applies an implementation of [requiredArtifacts] that only
/// downloads artifacts corresponding to an attached device.
/// downloads artifacts corresponding to potentially connected devices.
mixin DeviceBasedDevelopmentArtifacts on FlutterCommand {
@override
Future<Set<DevelopmentArtifact>> get requiredArtifacts async {
// If there are no attached devices, use the default configuration.
// Otherwise, only add development artifacts which correspond to a
// connected device.
final List<Device> devices = await globals.deviceManager!.getDevices();
// If there are no devices, use the default configuration.
// Otherwise, only add development artifacts corresponding to
// potentially connected devices. We might not be able to determine if a
// device is connected yet, so include it in case it becomes connected.
final List<Device> devices = await globals.deviceManager!.getDevices(
filter: DeviceDiscoveryFilter(excludeDisconnected: false),
);
if (devices.isEmpty) {
return super.requiredArtifacts;
}

View file

@ -2,20 +2,49 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/common.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/terminal.dart';
import '../base/user_messages.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../ios/devices.dart';
const String _checkingForWirelessDevicesMessage = 'Checking for wireless devices...';
const String _connectedDevicesMessage = 'Connected devices:';
const String _noAttachedCheckForWireless = 'No devices found yet. Checking for wireless devices...';
const String _noWirelessDevicesFoundMessage = 'No wireless devices were found.';
const String _wirelesslyConnectedDevicesMessage = 'Wirelessly connected devices:';
String _foundMultipleSpecifiedDevices(String deviceId) =>
'Found multiple devices with name or id matching $deviceId:';
/// This class handles functionality of finding and selecting target devices.
///
/// Target devices are devices that are supported and selectable to run
/// a flutter application on.
class TargetDevices {
TargetDevices({
factory TargetDevices({
required Platform platform,
required DeviceManager deviceManager,
required Logger logger,
}) {
if (platform.isMacOS) {
return TargetDevicesWithExtendedWirelessDeviceDiscovery(
deviceManager: deviceManager,
logger: logger,
);
}
return TargetDevices._private(
deviceManager: deviceManager,
logger: logger,
);
}
TargetDevices._private({
required DeviceManager deviceManager,
required Logger logger,
}) : _deviceManager = deviceManager,
@ -48,9 +77,11 @@ class TargetDevices {
Future<List<Device>> _getDeviceById({
bool includeDevicesUnsupportedByProject = false,
bool includeDisconnected = false,
}) async {
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
excludeDisconnected: !includeDisconnected,
supportFilter: _deviceManager.deviceSupportFilter(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
),
@ -66,6 +97,10 @@ class TargetDevices {
);
}
void startExtendedWirelessDeviceDiscovery({
Duration? deviceDiscoveryTimeout,
}) {}
/// Find and return all target [Device]s based upon criteria entered by the
/// user on the command line.
///
@ -235,7 +270,7 @@ class TargetDevices {
_deviceManager.specifiedDeviceId!,
));
} else {
_logger.printStatus(userMessages.flutterMultipleDevicesFound);
_logger.printStatus(_connectedDevicesMessage);
}
await Device.printDevices(attachedDevices, _logger);
@ -249,7 +284,8 @@ class TargetDevices {
final Device chosenDevice = await _chooseOneOfAvailableDevices(allDevices);
// Update the [DeviceManager.specifiedDeviceId] so that the user will not be prompted again.
// Update the [DeviceManager.specifiedDeviceId] so that the user will not
// be prompted again.
_deviceManager.specifiedDeviceId = chosenDevice.id;
return <Device>[chosenDevice];
@ -302,3 +338,406 @@ class TargetDevices {
return result;
}
}
@visibleForTesting
class TargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevices {
TargetDevicesWithExtendedWirelessDeviceDiscovery({
required super.deviceManager,
required super.logger,
}) : super._private();
Future<void>? _wirelessDevicesRefresh;
@visibleForTesting
bool waitForWirelessBeforeInput = false;
@visibleForTesting
late final TargetDeviceSelection deviceSelection = TargetDeviceSelection(_logger);
@override
void startExtendedWirelessDeviceDiscovery({
Duration? deviceDiscoveryTimeout,
}) {
if (deviceDiscoveryTimeout == null) {
_wirelessDevicesRefresh ??= _deviceManager.refreshExtendedWirelessDeviceDiscoverers(
timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout,
);
}
return;
}
Future<List<Device>> _getRefreshedWirelessDevices({
bool includeDevicesUnsupportedByProject = false,
}) async {
startExtendedWirelessDeviceDiscovery();
return () async {
await _wirelessDevicesRefresh;
return _deviceManager.getDevices(
filter: DeviceDiscoveryFilter(
deviceConnectionInterface: DeviceConnectionInterface.wireless,
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
),
);
}();
}
Future<Device?> _waitForIOSDeviceToConnect(IOSDevice device) async {
for (final DeviceDiscovery discoverer in _deviceManager.deviceDiscoverers) {
if (discoverer is IOSDevices) {
_logger.printStatus('Waiting for ${device.name} to connect...');
final Status waitingStatus = _logger.startSpinner(
timeout: const Duration(seconds: 30),
warningColor: TerminalColor.red,
slowWarningCallback: () {
return 'The device was unable to connect after 30 seconds. Ensure the device is paired and unlocked.';
},
);
final Device? connectedDevice = await discoverer.waitForDeviceToConnect(device, _logger);
waitingStatus.stop();
return connectedDevice;
}
}
return null;
}
/// Find and return all target [Device]s based upon criteria entered by the
/// user on the command line.
///
/// When the user has specified `all` devices, return all devices meeting criteria.
///
/// When the user has specified a device id/name, attempt to find an exact or
/// partial match. If an exact match or a single partial match is found and
/// the device is connected, return it immediately. If an exact match or a
/// single partial match is found and the device is not connected and it's
/// an iOS device, wait for it to connect.
///
/// When multiple devices are found and there is a terminal attached to
/// stdin, allow the user to select which device to use. When a terminal
/// with stdin is not available, print a list of available devices and
/// return null.
///
/// When no devices meet user specifications, print a list of unsupported
/// devices and return null.
@override
Future<List<Device>?> findAllTargetDevices({
Duration? deviceDiscoveryTimeout,
bool includeDevicesUnsupportedByProject = false,
}) async {
if (!globals.doctor!.canLaunchAnything) {
_logger.printError(userMessages.flutterNoDevelopmentDevice);
return null;
}
// When a user defines the timeout, use the super function that does not do
// longer wireless device discovery and does not wait for devices to connect.
if (deviceDiscoveryTimeout != null) {
return super.findAllTargetDevices(
deviceDiscoveryTimeout: deviceDiscoveryTimeout,
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
}
// Start polling for wireless devices that need longer to load if it hasn't
// already been started.
startExtendedWirelessDeviceDiscovery();
if (_deviceManager.hasSpecifiedDeviceId) {
// Get devices matching the specified device regardless of whether they
// are currently connected or not.
// If there is a single matching connected device, return it immediately.
// If the only device found is an iOS device that is not connected yet,
// wait for it to connect.
// If there are multiple matches, continue on to wait for all attached
// and wireless devices to load so the user can select between all
// connected matches.
final List<Device> devices = await _getDeviceById(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
includeDisconnected: true,
);
if (devices.length == 1) {
Device? matchedDevice = devices.first;
if (!matchedDevice.isConnected && matchedDevice is IOSDevice) {
matchedDevice = await _waitForIOSDeviceToConnect(matchedDevice);
}
if (matchedDevice != null && matchedDevice.isConnected) {
return <Device>[matchedDevice];
}
}
}
final List<Device> attachedDevices = await _getAttachedDevices(
supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject),
);
// _getRefreshedWirelessDevices must be run after _getAttachedDevices is
// finished to prevent non-iOS discoverers from running simultaneously.
// `AndroidDevices` may error if run simultaneously.
final Future<List<Device>> futureWirelessDevices = _getRefreshedWirelessDevices(
includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject,
);
if (attachedDevices.isEmpty) {
return _handleNoAttachedDevices(attachedDevices, futureWirelessDevices);
} else if (_deviceManager.hasSpecifiedAllDevices) {
return _handleAllDevices(attachedDevices, futureWirelessDevices);
}
// Even if there's only a single attached device, continue to
// `_handleRemainingDevices` since there might be wireless devices
// that are not loaded yet.
return _handleRemainingDevices(attachedDevices, futureWirelessDevices);
}
/// When no supported attached devices are found, wait for wireless devices
/// to load.
///
/// If no wireless devices are found, continue to `_handleNoDevices`.
///
/// If wireless devices are found, continue to `_handleMultipleDevices`.
Future<List<Device>?> _handleNoAttachedDevices(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
_logger.printStatus(_noAttachedCheckForWireless);
final List<Device> wirelessDevices = await futureWirelessDevices;
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (allDevices.isEmpty) {
_logger.printStatus('');
return _handleNoDevices();
} else if (_deviceManager.hasSpecifiedAllDevices) {
return allDevices;
} else if (allDevices.length > 1) {
_logger.printStatus('');
return _handleMultipleDevices(attachedDevices, wirelessDevices);
}
return allDevices;
}
/// Wait for wireless devices to load and then return all attached and
/// wireless devices.
Future<List<Device>?> _handleAllDevices(
List<Device> devices,
Future<List<Device>> futureWirelessDevices,
) async {
_logger.printStatus(_checkingForWirelessDevicesMessage);
final List<Device> wirelessDevices = await futureWirelessDevices;
return devices + wirelessDevices;
}
/// Determine which device to use when one or more are found.
///
/// If user has not specified a device id/name, attempt to prioritize
/// ephemeral devices. If a single ephemeral device is found, return it
/// immediately.
///
/// Otherwise, prompt the user to select a device if there is a terminal
/// with stdin. If there is not a terminal, display the list of devices with
/// instructions to use a device selection flag.
Future<List<Device>?> _handleRemainingDevices(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
final Device? ephemeralDevice = _deviceManager.getSingleEphemeralDevice(attachedDevices);
if (ephemeralDevice != null) {
return <Device>[ephemeralDevice];
}
if (!globals.terminal.stdinHasTerminal || !_logger.supportsColor) {
_logger.printStatus(_checkingForWirelessDevicesMessage);
final List<Device> wirelessDevices = await futureWirelessDevices;
if (attachedDevices.length + wirelessDevices.length == 1) {
return attachedDevices + wirelessDevices;
}
_logger.printStatus('');
// If the terminal has stdin but does not support color/ANSI (which is
// needed to clear lines), fallback to standard selection of device.
if (globals.terminal.stdinHasTerminal && !_logger.supportsColor) {
return _handleMultipleDevices(attachedDevices, wirelessDevices);
}
// If terminal does not have stdin, print out device list.
return _printMultipleDevices(attachedDevices, wirelessDevices);
}
return _selectFromDevicesAndCheckForWireless(
attachedDevices,
futureWirelessDevices,
);
}
/// Display a list of selectable attached devices and prompt the user to
/// choose one.
///
/// Also, display a message about waiting for wireless devices to load. Once
/// wireless devices have loaded, update waiting message, device list, and
/// selection options.
///
/// Wait for the user to select a device.
Future<List<Device>?> _selectFromDevicesAndCheckForWireless(
List<Device> attachedDevices,
Future<List<Device>> futureWirelessDevices,
) async {
if (attachedDevices.length == 1 || !_deviceManager.hasSpecifiedDeviceId) {
_logger.printStatus(_connectedDevicesMessage);
} else if (_deviceManager.hasSpecifiedDeviceId) {
// Multiple devices were found with part of the name/id provided.
_logger.printStatus(_foundMultipleSpecifiedDevices(
_deviceManager.specifiedDeviceId!,
));
}
// Display list of attached devices.
await Device.printDevices(attachedDevices, _logger);
// Display waiting message.
_logger.printStatus('');
_logger.printStatus(_checkingForWirelessDevicesMessage);
_logger.printStatus('');
// Start user device selection so user can select device while waiting
// for wireless devices to load if they want.
_displayDeviceOptions(attachedDevices);
deviceSelection.devices = attachedDevices;
final Future<Device> futureChosenDevice = deviceSelection.userSelectDevice();
Device? chosenDevice;
// Once wireless devices are found, we clear out the waiting message (3),
// device option list (attachedDevices.length), and device option prompt (1).
int numLinesToClear = attachedDevices.length + 4;
futureWirelessDevices = futureWirelessDevices.then((List<Device> wirelessDevices) async {
// If device is already chosen, don't update terminal with
// wireless device list.
if (chosenDevice != null) {
return wirelessDevices;
}
final List<Device> allDevices = attachedDevices + wirelessDevices;
if (_logger.isVerbose) {
await _verbosePrintWirelessDevices(attachedDevices, wirelessDevices);
} else {
// Also clear any invalid device selections.
numLinesToClear += deviceSelection.invalidAttempts;
await _printWirelessDevices(wirelessDevices, numLinesToClear);
}
_logger.printStatus('');
// Reprint device option list.
_displayDeviceOptions(allDevices);
deviceSelection.devices = allDevices;
// Reprint device option prompt.
_logger.printStatus(
'${userMessages.flutterChooseOne}: ',
emphasis: true,
newline: false,
);
return wirelessDevices;
});
// Used for testing.
if (waitForWirelessBeforeInput) {
await futureWirelessDevices;
}
// Wait for user to select a device.
chosenDevice = await futureChosenDevice;
// Update the [DeviceManager.specifiedDeviceId] so that the user will not
// be prompted again.
_deviceManager.specifiedDeviceId = chosenDevice.id;
return <Device>[chosenDevice];
}
/// Reprint list of attached devices before printing list of wireless devices.
Future<void> _verbosePrintWirelessDevices(
List<Device> attachedDevices,
List<Device> wirelessDevices,
) async {
if (wirelessDevices.isEmpty) {
_logger.printStatus(_noWirelessDevicesFoundMessage);
}
// The iOS xcdevice outputs once wireless devices are done loading, so
// reprint attached devices so they're grouped with the wireless ones.
_logger.printStatus(_connectedDevicesMessage);
await Device.printDevices(attachedDevices, _logger);
if (wirelessDevices.isNotEmpty) {
_logger.printStatus('');
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(wirelessDevices, _logger);
}
}
/// Clear [numLinesToClear] lines from terminal. Print message and list of
/// wireless devices.
Future<void> _printWirelessDevices(
List<Device> wirelessDevices,
int numLinesToClear,
) async {
_logger.printStatus(
globals.terminal.clearLines(numLinesToClear),
newline: false,
);
_logger.printStatus('');
if (wirelessDevices.isEmpty) {
_logger.printStatus(_noWirelessDevicesFoundMessage);
} else {
_logger.printStatus(_wirelesslyConnectedDevicesMessage);
await Device.printDevices(wirelessDevices, _logger);
}
}
}
@visibleForTesting
class TargetDeviceSelection {
TargetDeviceSelection(this._logger);
List<Device> devices = <Device>[];
final Logger _logger;
int invalidAttempts = 0;
/// Prompt user to select a device and wait until they select a valid device.
///
/// If the user selects `q`, exit the tool.
///
/// If the user selects an invalid number, reprompt them and continue waiting.
Future<Device> userSelectDevice() async {
Device? chosenDevice;
while (chosenDevice == null) {
final String userInputString = await readUserInput();
if (userInputString.toLowerCase() == 'q') {
throwToolExit('');
}
final int deviceIndex = int.parse(userInputString) - 1;
if (deviceIndex < devices.length) {
chosenDevice = devices[deviceIndex];
}
}
return chosenDevice;
}
/// Prompt user to select a device and wait until they select a valid
/// character.
///
/// Only allow input of a number or `q`.
@visibleForTesting
Future<String> readUserInput() async {
final RegExp pattern = RegExp(r'\d+$|q', caseSensitive: false);
final String prompt = userMessages.flutterChooseOne;
String? choice;
globals.terminal.singleCharMode = true;
while (choice == null || choice.length > 1 || !pattern.hasMatch(choice)) {
_logger.printStatus(prompt, emphasis: true, newline: false);
// prompt ends with ': '
_logger.printStatus(': ', emphasis: true, newline: false);
choice = (await globals.terminal.keystrokes.first).trim();
_logger.printStatus(choice);
invalidAttempts++;
}
globals.terminal.singleCharMode = false;
return choice;
}
}

View file

@ -1137,6 +1137,7 @@ class StreamLogger extends Logger {
VoidCallback? onFinish,
Duration? timeout,
SlowWarningCallback? slowWarningCallback,
TerminalColor? warningColor,
}) {
return SilentStatus(
stopwatch: Stopwatch(),
@ -1353,6 +1354,9 @@ class FakeIOSDevice extends Fake implements IOSDevice {
@override
bool get isConnected => true;
@override
bool get ephemeral => true;
}
class FakeMDnsClient extends Fake implements MDnsClient {

View file

@ -850,6 +850,9 @@ class FakeAndroidDevice extends Fake implements AndroidDevice {
@override
final bool ephemeral = false;
@override
final bool isConnected = true;
@override
Future<String> get sdkNameAndVersion async => 'Android 12';

View file

@ -6,12 +6,16 @@ import 'dart:convert';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/terminal.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/commands/devices.dart';
import 'package:flutter_tools/src/device.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:test/fake.dart';
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_devices.dart';
import '../../src/test_flutter_command_runner.dart';
@ -25,25 +29,54 @@ void main() {
late Cache cache;
late Platform platform;
setUp(() {
cache = Cache.test(processManager: FakeProcessManager.any());
platform = FakePlatform();
group('ensure factory', () {
late FakeBufferLogger fakeLogger;
setUpAll(() {
fakeLogger = FakeBufferLogger();
});
testWithoutContext('returns DevicesCommandOutputWithExtendedWirelessDeviceDiscovery on MacOS', () async {
final Platform platform = FakePlatform(operatingSystem: 'macos');
final DevicesCommandOutput devicesCommandOutput = DevicesCommandOutput(
platform: platform,
logger: fakeLogger,
);
expect(devicesCommandOutput is DevicesCommandOutputWithExtendedWirelessDeviceDiscovery, true);
});
testWithoutContext('returns default when not on MacOS', () async {
final Platform platform = FakePlatform();
final DevicesCommandOutput devicesCommandOutput = DevicesCommandOutput(
platform: platform,
logger: fakeLogger,
);
expect(devicesCommandOutput is DevicesCommandOutputWithExtendedWirelessDeviceDiscovery, false);
});
});
testUsingContext('returns 0 when called', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
}, overrides: <Type, Generator>{
Cache: () => cache,
Artifacts: () => Artifacts.test(),
});
group('when Platform is not MacOS', () {
setUp(() {
cache = Cache.test(processManager: FakeProcessManager.any());
platform = FakePlatform();
});
testUsingContext('no error when no connected devices', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(
testLogger.statusText,
equals('''
testUsingContext('returns 0 when called', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
}, overrides: <Type, Generator>{
Cache: () => cache,
Artifacts: () => Artifacts.test(),
});
testUsingContext('no error when no connected devices', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(
testLogger.statusText,
equals('''
No devices detected.
Run "flutter emulators" to list and start any available device emulators.
@ -51,127 +84,534 @@ Run "flutter emulators" to list and start any available device emulators.
If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips.
'''),
);
}, overrides: <Type, Generator>{
AndroidSdk: () => null,
DeviceManager: () => NoDevicesManager(),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
});
group('when includes both attached and wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
fakeDevices[2],
];
});
testUsingContext("get devices' platform types", () async {
final List<String> platformTypes = Device.devicesPlatformTypes(
await globals.deviceManager!.getAllDevices(),
);
expect(platformTypes, <String>['android', 'web']);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
AndroidSdk: () => null,
DeviceManager: () => NoDevicesManager(),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('Outputs parsable JSON with --machine flag', () async {
group('when includes both attached and wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
fakeDevices[2],
];
});
testUsingContext("get devices' platform types", () async {
final List<String> platformTypes = Device.devicesPlatformTypes(
await globals.deviceManager!.getAllDevices(),
);
expect(platformTypes, <String>['android', 'web']);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('Outputs parsable JSON with --machine flag', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices', '--machine']);
expect(
json.decode(testLogger.statusText),
<Map<String, Object>>[
fakeDevices[0].json,
fakeDevices[1].json,
fakeDevices[2].json,
],
);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
1 wirelessly connected device:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
});
group('when includes only attached devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
];
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
});
group('when includes only wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[2],
];
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
1 wirelessly connected device:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
});
});
group('when Platform is MacOS', () {
setUp(() {
cache = Cache.test(processManager: FakeProcessManager.any());
platform = FakePlatform(operatingSystem: 'macos');
});
testUsingContext('returns 0 when called', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices', '--machine']);
await createTestCommandRunner(command).run(<String>['devices']);
}, overrides: <Type, Generator>{
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('no error when no connected devices', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(
json.decode(testLogger.statusText),
<Map<String, Object>>[
fakeDevices[0].json,
fakeDevices[1].json,
fakeDevices[2].json,
],
testLogger.statusText,
equals('''
No devices found yet. Checking for wireless devices...
No devices detected.
Run "flutter emulators" to list and start any available device emulators.
If you expected your device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try increasing the time to wait for connected devices with the --device-timeout flag. Visit https://flutter.dev/setup/ for troubleshooting tips.
'''),
);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
AndroidSdk: () => null,
DeviceManager: () => NoDevicesManager(),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
group('when includes both attached and wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
fakeDevices[2],
fakeDevices[3],
];
});
testUsingContext("get devices' platform types", () async {
final List<String> platformTypes = Device.devicesPlatformTypes(
await globals.deviceManager!.getAllDevices(),
);
expect(platformTypes, <String>['android', 'ios', 'web']);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('Outputs parsable JSON with --machine flag', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices', '--machine']);
expect(
json.decode(testLogger.statusText),
<Map<String, Object>>[
fakeDevices[0].json,
fakeDevices[1].json,
fakeDevices[2].json,
fakeDevices[3].json,
],
);
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Cache: () => cache,
Artifacts: () => Artifacts.test(),
Platform: () => platform,
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
1 wirelessly connected device:
Checking for wireless devices...
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
});
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
group('when includes only attached devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
];
});
group('with ansi terminal', () {
late FakeTerminal terminal;
late FakeBufferLogger fakeLogger;
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
setUp(() {
terminal = FakeTerminal(supportsColor: true);
fakeLogger = FakeBufferLogger(terminal: terminal);
fakeLogger.originalStatusText = '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
});
Checking for wireless devices...
''';
});
group('when includes only wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[2],
];
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
1 wirelessly connected device:
expect(fakeLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
}, overrides: <Type, Generator>{
DeviceManager: () =>
_FakeDeviceManager(devices: deviceList, logger: fakeLogger),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
AnsiTerminal: () => terminal,
Logger: () => fakeLogger,
});
});
group('with verbose logging', () {
late FakeBufferLogger fakeLogger;
setUp(() {
fakeLogger = FakeBufferLogger(verbose: true);
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(fakeLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Checking for wireless devices...
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(
devices: deviceList,
logger: fakeLogger,
),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
Logger: () => fakeLogger,
});
});
});
group('when includes only attached devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[0],
fakeDevices[1],
];
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Checking for wireless devices...
No wireless devices were found.
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
group('with ansi terminal', () {
late FakeTerminal terminal;
late FakeBufferLogger fakeLogger;
setUp(() {
terminal = FakeTerminal(supportsColor: true);
fakeLogger = FakeBufferLogger(terminal: terminal);
fakeLogger.originalStatusText = '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Checking for wireless devices...
''';
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(fakeLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
No wireless devices were found.
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(
devices: deviceList,
logger: fakeLogger,
),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
AnsiTerminal: () => terminal,
Logger: () => fakeLogger,
});
});
group('with verbose logging', () {
late FakeBufferLogger fakeLogger;
setUp(() {
fakeLogger = FakeBufferLogger(verbose: true);
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(fakeLogger.statusText, '''
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
Checking for wireless devices...
2 connected devices:
ephemeral (mobile) ephemeral android-arm Test SDK (1.2.3) (emulator)
webby (mobile) webby web-javascript Web SDK (1.2.4) (emulator)
No wireless devices were found.
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(
devices: deviceList,
logger: fakeLogger,
),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
Logger: () => fakeLogger,
});
});
});
group('when includes only wireless devices', () {
List<FakeDeviceJsonData>? deviceList;
setUp(() {
deviceList = <FakeDeviceJsonData>[
fakeDevices[2],
fakeDevices[3],
];
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(testLogger.statusText, '''
No devices found yet. Checking for wireless devices...
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(devices: deviceList),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
});
group('with ansi terminal', () {
late FakeTerminal terminal;
late FakeBufferLogger fakeLogger;
setUp(() {
terminal = FakeTerminal(supportsColor: true);
fakeLogger = FakeBufferLogger(terminal: terminal);
fakeLogger.originalStatusText = '''
No devices found yet. Checking for wireless devices...
''';
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(fakeLogger.statusText, '''
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(
devices: deviceList,
logger: fakeLogger,
),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
AnsiTerminal: () => terminal,
Logger: () => fakeLogger,
});
});
group('with verbose logging', () {
late FakeBufferLogger fakeLogger;
setUp(() {
fakeLogger = FakeBufferLogger(verbose: true);
});
testUsingContext('available devices and diagnostics', () async {
final DevicesCommand command = DevicesCommand();
await createTestCommandRunner(command).run(<String>['devices']);
expect(fakeLogger.statusText, '''
No devices found yet. Checking for wireless devices...
2 wirelessly connected devices:
wireless android (mobile) wireless-android android-arm Test SDK (1.2.3) (emulator)
wireless ios (mobile) wireless-ios ios iOS 16 (simulator)
Cannot connect to device ABC
''');
}, overrides: <Type, Generator>{
DeviceManager: () => _FakeDeviceManager(
devices: deviceList,
logger: fakeLogger,
),
ProcessManager: () => FakeProcessManager.any(),
Platform: () => platform,
Logger: () => fakeLogger,
});
});
});
});
});
@ -180,8 +620,9 @@ wireless android (mobile) • wireless-android • android-arm • Test SDK (1.2
class _FakeDeviceManager extends DeviceManager {
_FakeDeviceManager({
List<FakeDeviceJsonData>? devices,
FakeBufferLogger? logger,
}) : fakeDevices = devices ?? <FakeDeviceJsonData>[],
super(logger: testLogger);
super(logger: logger ?? testLogger);
List<FakeDeviceJsonData> fakeDevices = <FakeDeviceJsonData>[];
@ -203,6 +644,12 @@ class _FakeDeviceManager extends DeviceManager {
DeviceDiscoveryFilter? filter,
}) => getAllDevices(filter: filter);
@override
Future<List<Device>> refreshExtendedWirelessDeviceDiscoverers({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) => getAllDevices(filter: filter);
@override
Future<List<String>> getDeviceDiagnostics() => Future<List<String>>.value(
<String>['Cannot connect to device ABC']
@ -215,18 +662,66 @@ class _FakeDeviceManager extends DeviceManager {
class NoDevicesManager extends DeviceManager {
NoDevicesManager() : super(logger: testLogger);
@override
Future<List<Device>> getAllDevices({
DeviceDiscoveryFilter? filter,
}) async => <Device>[];
@override
Future<List<Device>> refreshAllDevices({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) =>
getAllDevices();
@override
List<DeviceDiscovery> get deviceDiscoverers => <DeviceDiscovery>[];
}
class FakeTerminal extends Fake implements AnsiTerminal {
FakeTerminal({
this.supportsColor = false,
});
@override
final bool supportsColor;
@override
bool singleCharMode = false;
@override
String clearLines(int numberOfLines) {
return 'CLEAR_LINES_$numberOfLines';
}
}
class FakeBufferLogger extends BufferLogger {
FakeBufferLogger({
super.terminal,
super.outputPreferences,
super.verbose,
}) : super.test();
String originalStatusText = '';
@override
void printStatus(
String message, {
bool? emphasis,
TerminalColor? color,
bool? newline,
int? indent,
int? hangingIndent,
bool? wrap,
}) {
if (message.startsWith('CLEAR_LINES_')) {
expect(statusText, equals(originalStatusText));
final int numberOfLinesToRemove =
int.parse(message.split('CLEAR_LINES_')[1]) - 1;
final List<String> lines = LineSplitter.split(statusText).toList();
// Clear string buffer and re-add lines not removed
clear();
for (int lineNumber = 0; lineNumber < lines.length - numberOfLinesToRemove; lineNumber++) {
super.printStatus(lines[lineNumber]);
}
} else {
super.printStatus(
message,
emphasis: emphasis,
color: color,
newline: newline,
indent: indent,
hangingIndent: hangingIndent,
wrap: wrap,
);
}
}
}

View file

@ -1196,6 +1196,12 @@ class FakeDeviceManager extends Fake implements DeviceManager {
DeviceDiscoveryFilter? filter,
}) async => devices;
@override
Future<List<Device>> refreshAllDevices({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async => devices;
@override
Future<List<String>> getDeviceDiagnostics() async => diagnostics;
}

View file

@ -543,6 +543,9 @@ class ScreenshotDevice extends Fake implements Device {
@override
bool supportsScreenshot = true;
@override
bool get isConnected => true;
@override
Future<LaunchResult> startApp(
ApplicationPackage? package, {

View file

@ -268,6 +268,9 @@ class FakeAndroidDevice extends Fake implements AndroidDevice {
@override
final bool ephemeral = false;
@override
bool get isConnected => true;
@override
Future<String> get sdkNameAndVersion async => 'Android 12';

View file

@ -1170,6 +1170,9 @@ class FakeDevice extends Fake implements Device {
@override
bool get supportsFastStart => false;
@override
bool get ephemeral => true;
@override
bool get isConnected => true;

View file

@ -480,7 +480,7 @@ void main() {
expect(done, isTrue);
});
testWithoutContext('AnonymousSpinnerStatus logs warning after timeout', () async {
testWithoutContext('AnonymousSpinnerStatus logs warning after timeout without color support', () async {
mockStopwatch = FakeStopwatch();
const String warningMessage = 'a warning message.';
final bool done = FakeAsync().run<bool>((FakeAsync time) {
@ -489,6 +489,7 @@ void main() {
stopwatch: mockStopwatch,
terminal: terminal,
slowWarningCallback: () => warningMessage,
warningColor: TerminalColor.red,
timeout: const Duration(milliseconds: 100),
)..start();
// must be greater than the spinner timer duration
@ -497,6 +498,7 @@ void main() {
time.elapse(timeLapse);
List<String> lines = outputStdout();
expect(lines.join().contains(RegExp(red)), isFalse);
expect(lines.join(), '\ba warning message.⣻');
spinner.stop();
@ -506,6 +508,35 @@ void main() {
expect(done, isTrue);
});
testWithoutContext('AnonymousSpinnerStatus logs warning after timeout with color support', () async {
mockStopwatch = FakeStopwatch();
const String warningMessage = 'a warning message.';
final bool done = FakeAsync().run<bool>((FakeAsync time) {
final AnonymousSpinnerStatus spinner = AnonymousSpinnerStatus(
stdio: mockStdio,
stopwatch: mockStopwatch,
terminal: coloredTerminal,
slowWarningCallback: () => warningMessage,
warningColor: TerminalColor.red,
timeout: const Duration(milliseconds: 100),
)..start();
// must be greater than the spinner timer duration
const Duration timeLapse = Duration(milliseconds: 101);
mockStopwatch.elapsed += timeLapse;
time.elapse(timeLapse);
List<String> lines = outputStdout();
expect(lines.join().contains(RegExp(red)), isTrue);
expect(lines.join(), '\b${AnsiTerminal.red}a warning message.${AnsiTerminal.resetColor}');
expect(lines.join(), matches('$red$warningMessage$resetColor'));
spinner.stop();
lines = outputStdout();
return true;
});
expect(done, isTrue);
});
testWithoutContext('Stdout startProgress on colored terminal', () async {
final Logger logger = StdoutLogger(
terminal: coloredTerminal,

View file

@ -34,7 +34,7 @@ void main() {
});
});
group('ANSI coloring and bold', () {
group('ANSI coloring, bold, and clearing', () {
late AnsiTerminal terminal;
setUp(() {
@ -103,6 +103,39 @@ void main() {
equals('${AnsiTerminal.bold}bold output still bold${AnsiTerminal.resetBold}'),
);
});
testWithoutContext('clearing lines works', () {
expect(
terminal.clearLines(3),
equals(
'${AnsiTerminal.cursorBeginningOfLineCode}'
'${AnsiTerminal.clearEntireLineCode}'
'${AnsiTerminal.cursorUpLineCode}'
'${AnsiTerminal.clearEntireLineCode}'
'${AnsiTerminal.cursorUpLineCode}'
'${AnsiTerminal.clearEntireLineCode}'
),
);
expect(
terminal.clearLines(1),
equals(
'${AnsiTerminal.cursorBeginningOfLineCode}'
'${AnsiTerminal.clearEntireLineCode}'
),
);
});
testWithoutContext('clearing lines when color is not supported does not work', () {
terminal = AnsiTerminal(
stdio: Stdio(), // Danger, using real stdio.
platform: FakePlatform()..stdoutSupportsAnsi = false,
);
expect(
terminal.clearLines(3),
equals(''),
);
});
});
group('character input prompt', () {

View file

@ -141,29 +141,92 @@ void main() {
});
testWithoutContext('getAllDevices caches', () async {
final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f');
final TestDeviceManager deviceManager = TestDeviceManager(
<Device>[device1],
logger: BufferLogger.test(),
);
expect(await deviceManager.getAllDevices(), <Device>[device1]);
final FakePollingDeviceDiscovery notSupportedDiscoverer = FakePollingDeviceDiscovery();
final FakePollingDeviceDiscovery supportedDiscoverer = FakePollingDeviceDiscovery(requiresExtendedWirelessDeviceDiscovery: true);
final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e');
deviceManager.resetDevices(<Device>[device2]);
expect(await deviceManager.getAllDevices(), <Device>[device1]);
final FakeDevice attachedDevice = FakeDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice wirelessDevice = FakeDevice('Wireless device', 'wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
notSupportedDiscoverer.addDevice(attachedDevice);
supportedDiscoverer.addDevice(wirelessDevice);
final TestDeviceManager deviceManager = TestDeviceManager(
<Device>[],
logger: BufferLogger.test(),
deviceDiscoveryOverrides: <DeviceDiscovery>[
notSupportedDiscoverer,
supportedDiscoverer,
],
);
expect(await deviceManager.getAllDevices(), <Device>[attachedDevice, wirelessDevice]);
final FakeDevice newAttachedDevice = FakeDevice('Nexus 5X', '01abfc49119c410e');
notSupportedDiscoverer.addDevice(newAttachedDevice);
final FakeDevice newWirelessDevice = FakeDevice('New wireless device', 'new-wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
supportedDiscoverer.addDevice(newWirelessDevice);
expect(await deviceManager.getAllDevices(), <Device>[attachedDevice, wirelessDevice]);
});
testWithoutContext('refreshAllDevices does not cache', () async {
final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f');
final TestDeviceManager deviceManager = TestDeviceManager(
<Device>[device1],
logger: BufferLogger.test(),
);
expect(await deviceManager.refreshAllDevices(), <Device>[device1]);
final FakePollingDeviceDiscovery notSupportedDiscoverer = FakePollingDeviceDiscovery();
final FakePollingDeviceDiscovery supportedDiscoverer = FakePollingDeviceDiscovery(requiresExtendedWirelessDeviceDiscovery: true);
final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e');
deviceManager.resetDevices(<Device>[device2]);
expect(await deviceManager.refreshAllDevices(), <Device>[device2]);
final FakeDevice attachedDevice = FakeDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice wirelessDevice = FakeDevice('Wireless device', 'wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
notSupportedDiscoverer.addDevice(attachedDevice);
supportedDiscoverer.addDevice(wirelessDevice);
final TestDeviceManager deviceManager = TestDeviceManager(
<Device>[],
logger: BufferLogger.test(),
deviceDiscoveryOverrides: <DeviceDiscovery>[
notSupportedDiscoverer,
supportedDiscoverer,
],
);
expect(await deviceManager.refreshAllDevices(), <Device>[attachedDevice, wirelessDevice]);
final FakeDevice newAttachedDevice = FakeDevice('Nexus 5X', '01abfc49119c410e');
notSupportedDiscoverer.addDevice(newAttachedDevice);
final FakeDevice newWirelessDevice = FakeDevice('New wireless device', 'new-wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
supportedDiscoverer.addDevice(newWirelessDevice);
expect(await deviceManager.refreshAllDevices(), <Device>[attachedDevice, newAttachedDevice, wirelessDevice, newWirelessDevice]);
});
testWithoutContext('refreshExtendedWirelessDeviceDiscoverers only refreshes discoverers that require extended time', () async {
final FakePollingDeviceDiscovery normalDiscoverer = FakePollingDeviceDiscovery();
final FakePollingDeviceDiscovery extendedDiscoverer = FakePollingDeviceDiscovery(requiresExtendedWirelessDeviceDiscovery: true);
final FakeDevice attachedDevice = FakeDevice('Nexus 5', '0553790d0a4e726f');
final FakeDevice wirelessDevice = FakeDevice('Wireless device', 'wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
normalDiscoverer.addDevice(attachedDevice);
extendedDiscoverer.addDevice(wirelessDevice);
final TestDeviceManager deviceManager = TestDeviceManager(
<Device>[],
logger: BufferLogger.test(),
deviceDiscoveryOverrides: <DeviceDiscovery>[
normalDiscoverer,
extendedDiscoverer,
],
);
await deviceManager.refreshExtendedWirelessDeviceDiscoverers();
expect(await deviceManager.getAllDevices(), <Device>[attachedDevice, wirelessDevice]);
final FakeDevice newAttachedDevice = FakeDevice('Nexus 5X', '01abfc49119c410e');
normalDiscoverer.addDevice(newAttachedDevice);
final FakeDevice newWirelessDevice = FakeDevice('New wireless device', 'new-wireless-device', connectionInterface: DeviceConnectionInterface.wireless);
extendedDiscoverer.addDevice(newWirelessDevice);
await deviceManager.refreshExtendedWirelessDeviceDiscoverers();
expect(await deviceManager.getAllDevices(), <Device>[attachedDevice, wirelessDevice, newWirelessDevice]);
});
});
@ -1034,34 +1097,6 @@ class TestDeviceManager extends DeviceManager {
}
}
class MockDeviceDiscovery extends Fake implements DeviceDiscovery {
int devicesCalled = 0;
int discoverDevicesCalled = 0;
@override
bool supportsPlatform = true;
List<Device> deviceValues = <Device>[];
@override
Future<List<Device>> devices({DeviceDiscoveryFilter? filter}) async {
devicesCalled += 1;
return deviceValues;
}
@override
Future<List<Device>> discoverDevices({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async {
discoverDevicesCalled += 1;
return deviceValues;
}
@override
List<String> get wellKnownIds => <String>[];
}
class TestDeviceDiscoverySupportFilter extends DeviceDiscoverySupportFilter {
TestDeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject({
required super.flutterProject,

View file

@ -75,6 +75,7 @@ void main() {
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
expect(device.isSupported(), isTrue);
});
@ -91,6 +92,7 @@ void main() {
name: 'iPhone 1',
cpuArchitecture: DarwinArch.armv7,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
expect(device.isSupported(), isFalse);
});
@ -108,6 +110,7 @@ void main() {
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '1.0.0',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
).majorSdkVersion, 1);
expect(IOSDevice(
'device-123',
@ -121,6 +124,7 @@ void main() {
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '13.1.1',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
).majorSdkVersion, 13);
expect(IOSDevice(
'device-123',
@ -134,6 +138,7 @@ void main() {
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '10',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
).majorSdkVersion, 10);
expect(IOSDevice(
'device-123',
@ -147,6 +152,7 @@ void main() {
cpuArchitecture: DarwinArch.arm64,
sdkVersion: '0',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
).majorSdkVersion, 0);
expect(IOSDevice(
'device-123',
@ -160,6 +166,7 @@ void main() {
cpuArchitecture: DarwinArch.arm64,
sdkVersion: 'bogus',
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
).majorSdkVersion, 0);
});
@ -176,6 +183,7 @@ void main() {
sdkVersion: '13.3 17C54',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
@ -194,6 +202,7 @@ void main() {
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
expect(device.supportsRuntimeMode(BuildMode.debug), true);
@ -218,6 +227,7 @@ void main() {
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
},
throwsAssertionError,
@ -308,6 +318,7 @@ void main() {
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
logReader1 = createLogReader(device, appPackage1, process1);
logReader2 = createLogReader(device, appPackage2, process2);
@ -369,6 +380,7 @@ void main() {
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
device2 = IOSDevice(
@ -383,6 +395,7 @@ void main() {
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
});
@ -587,6 +600,120 @@ void main() {
expect(diagnostics.first, 'Generic pairing error');
});
});
group('waitForDeviceToConnect', () {
late FakeXcdevice xcdevice;
late Cache cache;
late FakeProcessManager fakeProcessManager;
late BufferLogger logger;
late IOSDeploy iosDeploy;
late IMobileDevice iMobileDevice;
late IOSWorkflow iosWorkflow;
late IOSDevice notConnected1;
setUp(() {
xcdevice = FakeXcdevice();
final Artifacts artifacts = Artifacts.test();
cache = Cache.test(processManager: FakeProcessManager.any());
logger = BufferLogger.test();
iosWorkflow = FakeIOSWorkflow();
fakeProcessManager = FakeProcessManager.any();
iosDeploy = IOSDeploy(
artifacts: artifacts,
cache: cache,
logger: logger,
platform: macPlatform,
processManager: fakeProcessManager,
);
iMobileDevice = IMobileDevice(
artifacts: artifacts,
cache: cache,
processManager: fakeProcessManager,
logger: logger,
);
notConnected1 = IOSDevice(
'00000001-0000000000000000',
name: 'iPad',
sdkVersion: '13.3',
cpuArchitecture: DarwinArch.arm64,
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
iosDeploy: iosDeploy,
iMobileDevice: iMobileDevice,
logger: logger,
platform: macPlatform,
fileSystem: MemoryFileSystem.test(),
connectionInterface: DeviceConnectionInterface.attached,
isConnected: false,
);
});
testWithoutContext('wait for device to connect via wifi', () async {
final IOSDevices iosDevices = IOSDevices(
platform: macPlatform,
xcdevice: xcdevice,
iosWorkflow: iosWorkflow,
logger: logger,
);
xcdevice.isInstalled = true;
xcdevice.waitForDeviceEvent = XCDeviceEventNotification(
XCDeviceEvent.attach,
XCDeviceEventInterface.wifi,
'00000001-0000000000000000'
);
final Device? device = await iosDevices.waitForDeviceToConnect(
notConnected1,
logger
);
expect(device?.isConnected, isTrue);
expect(device?.connectionInterface, DeviceConnectionInterface.wireless);
});
testWithoutContext('wait for device to connect via usb', () async {
final IOSDevices iosDevices = IOSDevices(
platform: macPlatform,
xcdevice: xcdevice,
iosWorkflow: iosWorkflow,
logger: logger,
);
xcdevice.isInstalled = true;
xcdevice.waitForDeviceEvent = XCDeviceEventNotification(
XCDeviceEvent.attach,
XCDeviceEventInterface.usb,
'00000001-0000000000000000'
);
final Device? device = await iosDevices.waitForDeviceToConnect(
notConnected1,
logger
);
expect(device?.isConnected, isTrue);
expect(device?.connectionInterface, DeviceConnectionInterface.attached);
});
testWithoutContext('wait for device returns null', () async {
final IOSDevices iosDevices = IOSDevices(
platform: macPlatform,
xcdevice: xcdevice,
iosWorkflow: iosWorkflow,
logger: logger,
);
xcdevice.isInstalled = true;
xcdevice.waitForDeviceEvent = null;
final Device? device = await iosDevices.waitForDeviceToConnect(
notConnected1,
logger
);
expect(device, isNull);
});
});
}
class FakeIOSApp extends Fake implements IOSApp {
@ -603,6 +730,7 @@ class FakeXcdevice extends Fake implements XCDevice {
final List<List<IOSDevice>> devices = <List<IOSDevice>>[];
final List<String> diagnostics = <String>[];
StreamController<Map<XCDeviceEvent, String>> deviceEventController = StreamController<Map<XCDeviceEvent, String>>();
XCDeviceEventNotification? waitForDeviceEvent;
@override
bool isInstalled = true;
@ -621,6 +749,16 @@ class FakeXcdevice extends Fake implements XCDevice {
Future<List<IOSDevice>> getAvailableIOSDevices({Duration? timeout}) async {
return devices[getAvailableIOSDevicesCount++];
}
@override
Future<XCDeviceEventNotification?> waitForDeviceToConnect(String deviceId) async {
final XCDeviceEventNotification? waitEvent = waitForDeviceEvent;
if (waitEvent != null) {
return XCDeviceEventNotification(waitEvent.eventType, waitEvent.eventInterface, waitEvent.deviceIdentifier);
} else {
return null;
}
}
}
class FakeProcess extends Fake implements Process {

View file

@ -359,5 +359,6 @@ IOSDevice setUpIOSDevice({
),
iProxy: IProxy.test(logger: logger, processManager: processManager),
connectionInterface: interfaceType ?? DeviceConnectionInterface.attached,
isConnected: true,
);
}

View file

@ -100,5 +100,6 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
cpuArchitecture: DarwinArch.arm64,
iProxy: IProxy.test(logger: logger, processManager: processManager),
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
}

View file

@ -338,6 +338,7 @@ IOSDevice setUpIOSDevice({
),
cpuArchitecture: DarwinArch.arm64,
connectionInterface: DeviceConnectionInterface.attached,
isConnected: true,
);
}

View file

@ -599,6 +599,7 @@ IOSDevice setUpIOSDevice({
),
cpuArchitecture: DarwinArch.arm64,
connectionInterface: interfaceType,
isConnected: true,
);
}

View file

@ -377,6 +377,150 @@ void main() {
await detach1.future;
expect(logger.traceText, contains('xcdevice observe error: Some error'));
});
testUsingContext('handles exit code', () async {
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'observe',
'--both',
],
));
final Completer<void> doneCompleter = Completer<void>();
xcdevice.observedDeviceEvents()!.listen(null, onDone: () {
doneCompleter.complete();
});
await doneCompleter.future;
expect(logger.traceText, contains('xcdevice exited with code 0'));
});
});
group('wait device events', () {
testUsingContext('relays events', () async {
const String deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
stdout: 'Attach: 00000001-0000000000000000\n',
));
// Attach: 00000001-0000000000000000
final XCDeviceEventNotification? event = await xcdevice.waitForDeviceToConnect(deviceId);
expect(event?.deviceIdentifier, deviceId);
expect(event?.eventInterface, XCDeviceEventInterface.wifi);
expect(event?.eventType, XCDeviceEvent.attach);
});
testUsingContext('handles exit code', () async {
const String deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
exitCode: 1,
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
));
final XCDeviceEventNotification? event = await xcdevice.waitForDeviceToConnect(deviceId);
expect(event, isNull);
expect(logger.traceText, contains('xcdevice wait --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice wait --wifi exited with code 0'));
expect(xcdevice.waitStreamController?.isClosed, isTrue);
});
testUsingContext('handles cancel', () async {
const String deviceId = '00000001-0000000000000000';
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--usb',
deviceId,
],
));
fakeProcessManager.addCommand(const FakeCommand(
command: <String>[
'script',
'-t',
'0',
'/dev/null',
'xcrun',
'xcdevice',
'wait',
'--wifi',
deviceId,
],
));
final Future<XCDeviceEventNotification?> futureEvent = xcdevice.waitForDeviceToConnect(deviceId);
xcdevice.cancelWaitForDeviceToConnect();
final XCDeviceEventNotification? event = await futureEvent;
expect(event, isNull);
expect(logger.traceText, contains('xcdevice wait --usb exited with code 0'));
expect(logger.traceText, contains('xcdevice wait --wifi exited with code 0'));
expect(xcdevice.waitStreamController?.isClosed, isTrue);
});
});
group('available devices', () {
@ -480,31 +624,41 @@ void main() {
stdout: devicesOutput,
));
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
expect(devices, hasLength(4));
expect(devices, hasLength(5));
expect(devices[0].id, '00008027-00192736010F802E');
expect(devices[0].name, 'An iPhone (Space Gray)');
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
expect(devices[0].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[0].isConnected, true);
expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
expect(devices[1].name, 'iPad 1');
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[1].cpuArchitecture, DarwinArch.armv7);
expect(devices[1].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[1].isConnected, true);
expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
expect(devices[2].name, 'A networked iPad');
expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
expect(devices[2].connectionInterface, DeviceConnectionInterface.wireless);
expect(devices[2].isConnected, true);
expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
expect(devices[3].name, 'iPad 2');
expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
expect(devices[3].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
expect(devices[3].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[3].isConnected, true);
expect(devices[4].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
expect(devices[4].name, 'iPhone');
expect(await devices[4].sdkNameAndVersion, 'iOS 13.3 17C54');
expect(devices[4].cpuArchitecture, DarwinArch.arm64);
expect(devices[4].connectionInterface, DeviceConnectionInterface.attached);
expect(devices[4].isConnected, false);
expect(fakeProcessManager, hasNoRemainingExpectations);
}, overrides: <Type, Generator>{

View file

@ -218,10 +218,17 @@ class FakeDeviceManager implements DeviceManager {
DeviceDiscoveryFilter? filter,
}) async => filteredDevices(filter);
@override
Future<List<Device>> refreshExtendedWirelessDeviceDiscoverers({
Duration? timeout,
DeviceDiscoveryFilter? filter,
}) async => filteredDevices(filter);
@override
Future<List<Device>> getDevicesById(
String deviceId, {
DeviceDiscoveryFilter? filter,
bool waitForDeviceToConnect = false,
}) async {
return filteredDevices(filter).where((Device device) {
return device.id == deviceId || device.id.startsWith(deviceId);
@ -231,6 +238,7 @@ class FakeDeviceManager implements DeviceManager {
@override
Future<List<Device>> getDevices({
DeviceDiscoveryFilter? filter,
bool waitForDeviceToConnect = false,
}) {
return hasSpecifiedDeviceId
? getDevicesById(specifiedDeviceId!, filter: filter)

View file

@ -79,6 +79,33 @@ List<FakeDeviceJsonData> fakeDevices = <FakeDeviceJsonData>[
},
}
),
FakeDeviceJsonData(
FakeDevice(
'wireless ios',
'wireless-ios',
type:PlatformType.ios,
connectionInterface: DeviceConnectionInterface.wireless,
)
..targetPlatform = Future<TargetPlatform>.value(TargetPlatform.ios)
..sdkNameAndVersion = Future<String>.value('iOS 16'),
<String,Object>{
'name': 'wireless ios',
'id': 'wireless-ios',
'isSupported': true,
'targetPlatform': 'ios',
'emulator': true,
'sdk': 'iOS 16',
'capabilities': <String, Object>{
'hotReload': true,
'hotRestart': true,
'screenshot': false,
'fastStart': false,
'flutterExit': true,
'hardwareRendering': true,
'startPaused': true,
},
},
),
];
/// Fake device to test `devices` command.
@ -167,7 +194,9 @@ class FakeDeviceJsonData {
}
class FakePollingDeviceDiscovery extends PollingDeviceDiscovery {
FakePollingDeviceDiscovery() : super('mock');
FakePollingDeviceDiscovery({
this.requiresExtendedWirelessDeviceDiscovery = false,
}) : super('mock');
final List<Device> _devices = <Device>[];
final StreamController<Device> _onAddedController = StreamController<Device>.broadcast();
@ -187,6 +216,9 @@ class FakePollingDeviceDiscovery extends PollingDeviceDiscovery {
@override
bool get canListAnything => true;
@override
bool requiresExtendedWirelessDeviceDiscovery;
void addDevice(Device device) {
_devices.add(device);
_onAddedController.add(device);