From fa01649a597091411458d2277b0ebe3aedf27a5a Mon Sep 17 00:00:00 2001 From: Victoria Ashworth Date: Wed, 29 Mar 2023 12:58:07 -0500 Subject: [PATCH] Update device selection to wait for wireless devices to load (#122932) Update device selection to wait for wireless devices to load --- .../flutter_tools/lib/src/base/logger.dart | 19 +- .../flutter_tools/lib/src/base/terminal.dart | 22 + .../lib/src/base/user_messages.dart | 1 - .../lib/src/commands/attach.dart | 3 + .../lib/src/commands/daemon.dart | 12 +- .../lib/src/commands/devices.dart | 158 +- .../lib/src/commands/install.dart | 3 + .../flutter_tools/lib/src/commands/logs.dart | 3 + .../flutter_tools/lib/src/commands/run.dart | 3 + .../lib/src/commands/screenshot.dart | 3 + packages/flutter_tools/lib/src/device.dart | 29 + packages/flutter_tools/lib/src/doctor.dart | 4 +- .../flutter_tools/lib/src/ios/devices.dart | 59 +- .../flutter_tools/lib/src/macos/xcdevice.dart | 241 ++- .../lib/src/runner/flutter_command.dart | 27 +- .../lib/src/runner/target_devices.dart | 445 +++- .../commands.shard/hermetic/attach_test.dart | 4 + .../commands.shard/hermetic/daemon_test.dart | 3 + .../commands.shard/hermetic/devices_test.dart | 713 ++++++- .../commands.shard/hermetic/doctor_test.dart | 6 + .../commands.shard/hermetic/drive_test.dart | 3 + .../hermetic/proxied_devices_test.dart | 3 + .../commands.shard/hermetic/run_test.dart | 3 + .../test/general.shard/base/logger_test.dart | 33 +- .../general.shard/base/terminal_test.dart | 35 +- .../test/general.shard/device_test.dart | 127 +- .../test/general.shard/ios/devices_test.dart | 138 ++ .../ios/ios_device_install_test.dart | 1 + .../ios/ios_device_project_test.dart | 1 + .../ios_device_start_nonprebuilt_test.dart | 1 + .../ios/ios_device_start_prebuilt_test.dart | 1 + .../test/general.shard/macos/xcode_test.dart | 158 +- .../runner/target_devices_test.dart | 1870 +++++++++++++++-- packages/flutter_tools/test/src/context.dart | 8 + .../flutter_tools/test/src/fake_devices.dart | 34 +- 35 files changed, 3808 insertions(+), 366 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/logger.dart b/packages/flutter_tools/lib/src/base/logger.dart index 71b3af7f17c..0b4dec079b7 100644 --- a/packages/flutter_tools/lib/src/base/logger.dart +++ b/packages/flutter_tools/lib/src/base/logger.dart @@ -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); } } diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart index 001bdfc6b9e..720b92595fc 100644 --- a/packages/flutter_tools/lib/src/base/terminal.dart +++ b/packages/flutter_tools/lib/src/base/terminal.dart @@ -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 _colorMap = { 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) { diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 31487b9fdc4..122392056a1 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -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 => diff --git a/packages/flutter_tools/lib/src/commands/attach.dart b/packages/flutter_tools/lib/src/commands/attach.dart index 21cae52f3c9..fe66eaf4f2f 100644 --- a/packages/flutter_tools/lib/src/commands/attach.dart +++ b/packages/flutter_tools/lib/src/commands/attach.dart @@ -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; diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index de4c252e92d..4caaa828448 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -878,12 +878,12 @@ class DeviceDomain extends Domain { final List _discoverers = []; - /// 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>> getDevices([ Map? args ]) async { return >[ 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.value(); } - /// Return the device matching the deviceId field in the args. + /// Return the connected device matching the deviceId field in the args. Future _getDevice(String? deviceId) async { for (final PollingDeviceDiscovery discoverer in _discoverers) { - final List devices = await discoverer.devices(); + final List devices = await discoverer.devices( + filter: DeviceDiscoveryFilter(), + ); Device? device; for (final Device localDevice in devices) { if (localDevice.id == deviceId) { diff --git a/packages/flutter_tools/lib/src/commands/devices.dart b/packages/flutter_tools/lib/src/commands/devices.dart index 8e8d6112c46..2b53bb24bfc 100644 --- a/packages/flutter_tools/lib/src/commands/devices.dart +++ b/packages/flutter_tools/lib/src/commands/devices.dart @@ -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> _getAttachedDevices(DeviceManager deviceManager) async { @@ -98,7 +131,7 @@ class DevicesCommandOutput { Future findAndOutputAllTargetDevices({required bool machine}) async { List attachedDevices = []; List wirelessDevices = []; - 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 _printDiagnostics() async { - final List diagnostics = await globals.deviceManager?.getDeviceDiagnostics() ?? []; + final List diagnostics = await _deviceManager?.getDeviceDiagnostics() ?? []; if (diagnostics.isNotEmpty) { - globals.printStatus(''); + _logger.printStatus(''); for (final String diagnostic in diagnostics) { - globals.printStatus('• $diagnostic', hangingIndent: 2); + _logger.printStatus('• $diagnostic', hangingIndent: 2); } } } Future printDevicesAsJson(List 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 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 devices = await _deviceManager?.refreshAllDevices( + filter: DeviceDiscoveryFilter(), + timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout, + ) ?? []; + await printDevicesAsJson(devices); + return; + } + + final Future? extendedWirelessDiscovery = _deviceManager?.refreshExtendedWirelessDeviceDiscoverers( + timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout, + ); + + List attachedDevices = []; + 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 wirelessDevices = []; + 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(); + } +} diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart index feb2741e424..ce6d30255d6 100644 --- a/packages/flutter_tools/lib/src/commands/install.dart +++ b/packages/flutter_tools/lib/src/commands/install.dart @@ -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'); diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart index 4a0171f7936..9abab3a2e73 100644 --- a/packages/flutter_tools/lib/src/commands/logs.dart +++ b/packages/flutter_tools/lib/src/commands/logs.dart @@ -30,6 +30,9 @@ class LogsCommand extends FlutterCommand { @override final String category = FlutterCommandCategory.tools; + @override + bool get refreshWirelessDevices => true; + @override Future> get requiredArtifacts async => const {}; diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 6ad64a3e26f..c0c3d113110 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -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; diff --git a/packages/flutter_tools/lib/src/commands/screenshot.dart b/packages/flutter_tools/lib/src/commands/screenshot.dart index f06f61288f0..3bb590aa52b 100644 --- a/packages/flutter_tools/lib/src/commands/screenshot.dart +++ b/packages/flutter_tools/lib/src/commands/screenshot.dart @@ -65,6 +65,9 @@ class ScreenshotCommand extends FlutterCommand { @override final String category = FlutterCommandCategory.tools; + @override + bool get refreshWirelessDevices => true; + @override final List aliases = ['pic']; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 071f056aec2..f620e19e526 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -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((List 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 refreshExtendedWirelessDeviceDiscoverers({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async { + await Future.wait>(>>[ + 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> 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> 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> discoverDevices({ Duration? timeout, diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 7e85efed028..5af78209778 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -680,7 +680,9 @@ class DeviceValidator extends DoctorValidator { @override Future validate() async { - final List devices = await _deviceManager.getAllDevices(); + final List devices = await _deviceManager.refreshAllDevices( + timeout: DeviceManager.minimumWirelessDeviceDiscoveryTimeout, + ); List installedMessages = []; if (devices.isNotEmpty) { installedMessages = (await Device.descriptions(devices)) diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index d06659ccd83..d3f9a5ea2e7 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -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>? _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(); // Start by populating all currently attached devices. - deviceNotifier!.updateWithNewList(await pollingGetDevices()); + final List devices = await pollingGetDevices(); + + // Only show connected devices. + final List 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 devices = await pollingGetDevices(); - notifier.updateWithNewList(devices); + + // Only show connected devices. + final List 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 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 _logReaders = {}; diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart index cfe91f1ce07..eff1bf8cb43 100644 --- a/packages/flutter_tools/lib/src/macos/xcdevice.dart +++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart @@ -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>? _deviceIdentifierByEvent; + @visibleForTesting + StreamController? 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(utf8.decoder) .transform(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.attach: identifier, - }); - } else if (verb.startsWith('detach')) { - _deviceIdentifierByEvent?.add({ - XCDeviceEvent.detach: identifier, - }); - } + final XCDeviceEventNotification? event = _processXCDeviceStdOut( + line, + XCDeviceEventInterface.usb, + ); + if (event != null) { + _deviceIdentifierByEvent?.add({ + event.eventType: event.deviceIdentifier, + }); } }); final StreamSubscription 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 waitForDeviceToConnect( + String deviceId, + ) async { + try { + if (_usbDeviceWaitProcess != null || _wifiDeviceWaitProcess != null) { + throw Exception('xcdevice wait restart failed'); + } + + waitStreamController = StreamController(); + + // Run in interactive mode (via script) to convince + // xcdevice it has a terminal attached in order to redirect stdout. + _usbDeviceWaitProcess = await _processUtils.start( + [ + 'script', + '-t', + '0', + '/dev/null', + ..._xcode.xcrunCommand(), + 'xcdevice', + 'wait', + '--${XCDeviceEventInterface.usb.name}', + deviceId, + ], + ); + + _wifiDeviceWaitProcess = await _processUtils.start( + [ + 'script', + '-t', + '0', + '/dev/null', + ..._xcode.xcrunCommand(), + 'xcdevice', + 'wait', + '--${XCDeviceEventInterface.wifi.name}', + deviceId, + ], + ); + + final StreamSubscription usbStdoutSubscription = _processWaitStdOut( + _usbDeviceWaitProcess!, + XCDeviceEventInterface.usb, + ); + final StreamSubscription wifiStdoutSubscription = _processWaitStdOut( + _wifiDeviceWaitProcess!, + XCDeviceEventInterface.wifi, + ); + + final StreamSubscription usbStderrSubscription = _usbDeviceWaitProcess!.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + _logger.printTrace('xcdevice wait --usb error: $line'); + }); + final StreamSubscription wifiStderrSubscription = _wifiDeviceWaitProcess!.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen((String line) { + _logger.printTrace('xcdevice wait --wifi error: $line'); + }); + + final Future 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 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 allProcessesExited = Future.wait( + >[ + usbProcessExited, + wifiProcessExited, + ]).whenComplete(() async { + _usbDeviceWaitProcess = null; + _wifiDeviceWaitProcess = null; + await waitStreamController?.close(); + }); + + return await Future.any( + >[ + 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 _processWaitStdOut( + Process process, + XCDeviceEventInterface eventInterface, + ) { + return process.stdout + .transform(utf8.decoder) + .transform(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> getAvailableIOSDevices({ Duration? timeout }) async { final List? allAvailableDevices = await _getAllDevices(timeout: timeout ?? const Duration(seconds: 2)); @@ -284,6 +478,7 @@ class XCDevice { continue; } + bool isConnected = true; final Map? 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. diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index ee3f00af85e..90f5e437b63 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -210,6 +210,11 @@ abstract class FlutterCommand extends Command { 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 { }(); late final TargetDevices _targetDevices = TargetDevices( + platform: globals.platform, deviceManager: globals.deviceManager!, logger: globals.logger, ); @@ -1466,6 +1472,14 @@ Run 'flutter -h' (or 'flutter -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 -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> 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 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 devices = await globals.deviceManager!.getDevices( + filter: DeviceDiscoveryFilter(excludeDisconnected: false), + ); if (devices.isEmpty) { return super.requiredArtifacts; } diff --git a/packages/flutter_tools/lib/src/runner/target_devices.dart b/packages/flutter_tools/lib/src/runner/target_devices.dart index a5cdca6e91f..e15bee8f22f 100644 --- a/packages/flutter_tools/lib/src/runner/target_devices.dart +++ b/packages/flutter_tools/lib/src/runner/target_devices.dart @@ -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> _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 [chosenDevice]; @@ -302,3 +338,406 @@ class TargetDevices { return result; } } + +@visibleForTesting +class TargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevices { + TargetDevicesWithExtendedWirelessDeviceDiscovery({ + required super.deviceManager, + required super.logger, + }) : super._private(); + + Future? _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> _getRefreshedWirelessDevices({ + bool includeDevicesUnsupportedByProject = false, + }) async { + startExtendedWirelessDeviceDiscovery(); + return () async { + await _wirelessDevicesRefresh; + return _deviceManager.getDevices( + filter: DeviceDiscoveryFilter( + deviceConnectionInterface: DeviceConnectionInterface.wireless, + supportFilter: _defaultSupportFilter(includeDevicesUnsupportedByProject), + ), + ); + }(); + } + + Future _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?> 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 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 [matchedDevice]; + } + } + } + + final List 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> 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?> _handleNoAttachedDevices( + List attachedDevices, + Future> futureWirelessDevices, + ) async { + _logger.printStatus(_noAttachedCheckForWireless); + + final List wirelessDevices = await futureWirelessDevices; + final List 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?> _handleAllDevices( + List devices, + Future> futureWirelessDevices, + ) async { + _logger.printStatus(_checkingForWirelessDevicesMessage); + final List 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?> _handleRemainingDevices( + List attachedDevices, + Future> futureWirelessDevices, + ) async { + final Device? ephemeralDevice = _deviceManager.getSingleEphemeralDevice(attachedDevices); + if (ephemeralDevice != null) { + return [ephemeralDevice]; + } + + if (!globals.terminal.stdinHasTerminal || !_logger.supportsColor) { + _logger.printStatus(_checkingForWirelessDevicesMessage); + final List 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?> _selectFromDevicesAndCheckForWireless( + List attachedDevices, + Future> 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 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 wirelessDevices) async { + // If device is already chosen, don't update terminal with + // wireless device list. + if (chosenDevice != null) { + return wirelessDevices; + } + + final List 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 [chosenDevice]; + } + + /// Reprint list of attached devices before printing list of wireless devices. + Future _verbosePrintWirelessDevices( + List attachedDevices, + List 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 _printWirelessDevices( + List 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 devices = []; + 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 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 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; + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart index d1ec789ab7d..2c0ec1d975e 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -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 { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart index a50433eb60e..5ac5e4249dd 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart @@ -850,6 +850,9 @@ class FakeAndroidDevice extends Fake implements AndroidDevice { @override final bool ephemeral = false; + @override + final bool isConnected = true; + @override Future get sdkNameAndVersion async => 'Android 12'; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart index 04f1c068dc2..3cefbaf833d 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart @@ -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(['devices']); - }, overrides: { - 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(['devices']); - expect( - testLogger.statusText, - equals(''' + testUsingContext('returns 0 when called', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['devices']); + }, overrides: { + Cache: () => cache, + Artifacts: () => Artifacts.test(), + }); + + testUsingContext('no error when no connected devices', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['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: { - AndroidSdk: () => null, - DeviceManager: () => NoDevicesManager(), - ProcessManager: () => FakeProcessManager.any(), - Cache: () => cache, - Artifacts: () => Artifacts.test(), - }); - - group('when includes both attached and wireless devices', () { - List? deviceList; - setUp(() { - deviceList = [ - fakeDevices[0], - fakeDevices[1], - fakeDevices[2], - ]; - }); - - testUsingContext("get devices' platform types", () async { - final List platformTypes = Device.devicesPlatformTypes( - await globals.deviceManager!.getAllDevices(), - ); - expect(platformTypes, ['android', 'web']); }, overrides: { - 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? deviceList; + setUp(() { + deviceList = [ + fakeDevices[0], + fakeDevices[1], + fakeDevices[2], + ]; + }); + + testUsingContext("get devices' platform types", () async { + final List platformTypes = Device.devicesPlatformTypes( + await globals.deviceManager!.getAllDevices(), + ); + expect(platformTypes, ['android', 'web']); + }, overrides: { + 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(['devices', '--machine']); + expect( + json.decode(testLogger.statusText), + >[ + fakeDevices[0].json, + fakeDevices[1].json, + fakeDevices[2].json, + ], + ); + }, overrides: { + 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(['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: { + DeviceManager: () => _FakeDeviceManager(devices: deviceList), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => platform, + }); + }); + + group('when includes only attached devices', () { + List? deviceList; + setUp(() { + deviceList = [ + fakeDevices[0], + fakeDevices[1], + ]; + }); + + testUsingContext('available devices and diagnostics', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['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: { + DeviceManager: () => _FakeDeviceManager(devices: deviceList), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => platform, + }); + }); + + group('when includes only wireless devices', () { + List? deviceList; + setUp(() { + deviceList = [ + fakeDevices[2], + ]; + }); + + testUsingContext('available devices and diagnostics', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['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: { + 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(['devices', '--machine']); + await createTestCommandRunner(command).run(['devices']); + }, overrides: { + Cache: () => cache, + Artifacts: () => Artifacts.test(), + Platform: () => platform, + }); + + testUsingContext('no error when no connected devices', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['devices']); expect( - json.decode(testLogger.statusText), - >[ - 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: { - 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(['devices']); - expect(testLogger.statusText, ''' + group('when includes both attached and wireless devices', () { + List? deviceList; + setUp(() { + deviceList = [ + fakeDevices[0], + fakeDevices[1], + fakeDevices[2], + fakeDevices[3], + ]; + }); + + testUsingContext("get devices' platform types", () async { + final List platformTypes = Device.devicesPlatformTypes( + await globals.deviceManager!.getAllDevices(), + ); + expect(platformTypes, ['android', 'ios', 'web']); + }, overrides: { + 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(['devices', '--machine']); + expect( + json.decode(testLogger.statusText), + >[ + fakeDevices[0].json, + fakeDevices[1].json, + fakeDevices[2].json, + fakeDevices[3].json, + ], + ); + }, overrides: { + 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(['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: { - DeviceManager: () => _FakeDeviceManager(devices: deviceList), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => platform, - }); - }); + }, overrides: { + DeviceManager: () => _FakeDeviceManager(devices: deviceList), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => platform, + }); - group('when includes only attached devices', () { - List? deviceList; - setUp(() { - deviceList = [ - 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(['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: { - DeviceManager: () => _FakeDeviceManager(devices: deviceList), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => platform, - }); - }); +Checking for wireless devices... +'''; + }); - group('when includes only wireless devices', () { - List? deviceList; - setUp(() { - deviceList = [ - fakeDevices[2], - ]; - }); + testUsingContext('available devices and diagnostics', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['devices']); - testUsingContext('available devices and diagnostics', () async { - final DevicesCommand command = DevicesCommand(); - await createTestCommandRunner(command).run(['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: { - DeviceManager: () => _FakeDeviceManager(devices: deviceList), - ProcessManager: () => FakeProcessManager.any(), - Platform: () => platform, + }, overrides: { + 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(['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: { + DeviceManager: () => _FakeDeviceManager( + devices: deviceList, + logger: fakeLogger, + ), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => platform, + Logger: () => fakeLogger, + }); + }); + }); + + group('when includes only attached devices', () { + List? deviceList; + setUp(() { + deviceList = [ + fakeDevices[0], + fakeDevices[1], + ]; + }); + + testUsingContext('available devices and diagnostics', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['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: { + 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(['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: { + 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(['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: { + DeviceManager: () => _FakeDeviceManager( + devices: deviceList, + logger: fakeLogger, + ), + ProcessManager: () => FakeProcessManager.any(), + Platform: () => platform, + Logger: () => fakeLogger, + }); + }); + }); + + group('when includes only wireless devices', () { + List? deviceList; + setUp(() { + deviceList = [ + fakeDevices[2], + fakeDevices[3], + ]; + }); + + testUsingContext('available devices and diagnostics', () async { + final DevicesCommand command = DevicesCommand(); + await createTestCommandRunner(command).run(['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: { + 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(['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: { + 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(['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: { + 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? devices, + FakeBufferLogger? logger, }) : fakeDevices = devices ?? [], - super(logger: testLogger); + super(logger: logger ?? testLogger); List fakeDevices = []; @@ -203,6 +644,12 @@ class _FakeDeviceManager extends DeviceManager { DeviceDiscoveryFilter? filter, }) => getAllDevices(filter: filter); + @override + Future> refreshExtendedWirelessDeviceDiscoverers({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) => getAllDevices(filter: filter); + @override Future> getDeviceDiagnostics() => Future>.value( ['Cannot connect to device ABC'] @@ -215,18 +662,66 @@ class _FakeDeviceManager extends DeviceManager { class NoDevicesManager extends DeviceManager { NoDevicesManager() : super(logger: testLogger); - @override - Future> getAllDevices({ - DeviceDiscoveryFilter? filter, - }) async => []; - - @override - Future> refreshAllDevices({ - Duration? timeout, - DeviceDiscoveryFilter? filter, - }) => - getAllDevices(); - @override List get deviceDiscoverers => []; } + +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 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, + ); + } + } +} diff --git a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart index a5ea59e6f00..85b214eaa56 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart @@ -1196,6 +1196,12 @@ class FakeDeviceManager extends Fake implements DeviceManager { DeviceDiscoveryFilter? filter, }) async => devices; + @override + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async => devices; + @override Future> getDeviceDiagnostics() async => diagnostics; } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart index 78b7ac9e9a8..9d1250389fa 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -543,6 +543,9 @@ class ScreenshotDevice extends Fake implements Device { @override bool supportsScreenshot = true; + @override + bool get isConnected => true; + @override Future startApp( ApplicationPackage? package, { diff --git a/packages/flutter_tools/test/commands.shard/hermetic/proxied_devices_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/proxied_devices_test.dart index 6612e93c36b..9a61faecbda 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/proxied_devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/proxied_devices_test.dart @@ -268,6 +268,9 @@ class FakeAndroidDevice extends Fake implements AndroidDevice { @override final bool ephemeral = false; + @override + bool get isConnected => true; + @override Future get sdkNameAndVersion async => 'Android 12'; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index e12d0db4f63..70a41fa8a5c 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -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; diff --git a/packages/flutter_tools/test/general.shard/base/logger_test.dart b/packages/flutter_tools/test/general.shard/base/logger_test.dart index f8f8dc08c01..29a142acdc5 100644 --- a/packages/flutter_tools/test/general.shard/base/logger_test.dart +++ b/packages/flutter_tools/test/general.shard/base/logger_test.dart @@ -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((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 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((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 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, diff --git a/packages/flutter_tools/test/general.shard/base/terminal_test.dart b/packages/flutter_tools/test/general.shard/base/terminal_test.dart index 51b0f799adb..3d335bbe254 100644 --- a/packages/flutter_tools/test/general.shard/base/terminal_test.dart +++ b/packages/flutter_tools/test/general.shard/base/terminal_test.dart @@ -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', () { diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index 46c0ea6a5b9..27745363082 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -141,29 +141,92 @@ void main() { }); testWithoutContext('getAllDevices caches', () async { - final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); - final TestDeviceManager deviceManager = TestDeviceManager( - [device1], - logger: BufferLogger.test(), - ); - expect(await deviceManager.getAllDevices(), [device1]); + final FakePollingDeviceDiscovery notSupportedDiscoverer = FakePollingDeviceDiscovery(); + final FakePollingDeviceDiscovery supportedDiscoverer = FakePollingDeviceDiscovery(requiresExtendedWirelessDeviceDiscovery: true); - final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); - deviceManager.resetDevices([device2]); - expect(await deviceManager.getAllDevices(), [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( + [], + logger: BufferLogger.test(), + deviceDiscoveryOverrides: [ + notSupportedDiscoverer, + supportedDiscoverer, + ], + ); + expect(await deviceManager.getAllDevices(), [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(), [attachedDevice, wirelessDevice]); }); testWithoutContext('refreshAllDevices does not cache', () async { - final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); - final TestDeviceManager deviceManager = TestDeviceManager( - [device1], - logger: BufferLogger.test(), - ); - expect(await deviceManager.refreshAllDevices(), [device1]); + final FakePollingDeviceDiscovery notSupportedDiscoverer = FakePollingDeviceDiscovery(); + final FakePollingDeviceDiscovery supportedDiscoverer = FakePollingDeviceDiscovery(requiresExtendedWirelessDeviceDiscovery: true); - final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); - deviceManager.resetDevices([device2]); - expect(await deviceManager.refreshAllDevices(), [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( + [], + logger: BufferLogger.test(), + deviceDiscoveryOverrides: [ + notSupportedDiscoverer, + supportedDiscoverer, + ], + ); + expect(await deviceManager.refreshAllDevices(), [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(), [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( + [], + logger: BufferLogger.test(), + deviceDiscoveryOverrides: [ + normalDiscoverer, + extendedDiscoverer, + ], + ); + await deviceManager.refreshExtendedWirelessDeviceDiscoverers(); + expect(await deviceManager.getAllDevices(), [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(), [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 deviceValues = []; - - @override - Future> devices({DeviceDiscoveryFilter? filter}) async { - devicesCalled += 1; - return deviceValues; - } - - @override - Future> discoverDevices({ - Duration? timeout, - DeviceDiscoveryFilter? filter, - }) async { - discoverDevicesCalled += 1; - return deviceValues; - } - - @override - List get wellKnownIds => []; -} - class TestDeviceDiscoverySupportFilter extends DeviceDiscoverySupportFilter { TestDeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject({ required super.flutterProject, diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart index f2edda82ba2..15d81b92e01 100644 --- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart @@ -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> devices = >[]; final List diagnostics = []; StreamController> deviceEventController = StreamController>(); + XCDeviceEventNotification? waitForDeviceEvent; @override bool isInstalled = true; @@ -621,6 +749,16 @@ class FakeXcdevice extends Fake implements XCDevice { Future> getAvailableIOSDevices({Duration? timeout}) async { return devices[getAvailableIOSDevicesCount++]; } + + @override + Future 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 { diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart index 50f02823874..91040b3fa63 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart @@ -359,5 +359,6 @@ IOSDevice setUpIOSDevice({ ), iProxy: IProxy.test(logger: logger, processManager: processManager), connectionInterface: interfaceType ?? DeviceConnectionInterface.attached, + isConnected: true, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart index 66efd65fd75..9b052900605 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart @@ -100,5 +100,6 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) { cpuArchitecture: DarwinArch.arm64, iProxy: IProxy.test(logger: logger, processManager: processManager), connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart index f6f72858fc0..ee1272c8612 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart @@ -338,6 +338,7 @@ IOSDevice setUpIOSDevice({ ), cpuArchitecture: DarwinArch.arm64, connectionInterface: DeviceConnectionInterface.attached, + isConnected: true, ); } diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart index 43743d87e6c..e78acdf188b 100644 --- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart @@ -599,6 +599,7 @@ IOSDevice setUpIOSDevice({ ), cpuArchitecture: DarwinArch.arm64, connectionInterface: interfaceType, + isConnected: true, ); } diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart index 3699d0de485..d5baff4356d 100644 --- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart @@ -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: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'observe', + '--both', + ], + )); + + final Completer doneCompleter = Completer(); + 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: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'wait', + '--usb', + deviceId, + ], + )); + fakeProcessManager.addCommand(const FakeCommand( + command: [ + '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: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'wait', + '--usb', + deviceId, + ], + exitCode: 1, + )); + fakeProcessManager.addCommand(const FakeCommand( + command: [ + '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: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'wait', + '--usb', + deviceId, + ], + )); + fakeProcessManager.addCommand(const FakeCommand( + command: [ + 'script', + '-t', + '0', + '/dev/null', + 'xcrun', + 'xcdevice', + 'wait', + '--wifi', + deviceId, + ], + )); + + final Future 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 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: { diff --git a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart index c442884a973..e4542813394 100644 --- a/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/runner/target_devices_test.dart @@ -8,8 +8,12 @@ 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/build_info.dart'; +import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/doctor.dart'; +import 'package:flutter_tools/src/ios/devices.dart'; +import 'package:flutter_tools/src/ios/ios_workflow.dart'; +import 'package:flutter_tools/src/macos/xcdevice.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/runner/target_devices.dart'; import 'package:test/fake.dart'; @@ -18,7 +22,41 @@ import '../../src/common.dart'; import '../../src/context.dart'; void main() { - group('findAllTargetDevices', () { + testWithoutContext('Ensure factory returns TargetDevicesWithExtendedWirelessDeviceDiscovery on MacOS', () async { + final BufferLogger logger = BufferLogger.test(); + final Platform platform = FakePlatform(operatingSystem: 'macos'); + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + + final TargetDevices targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); + + expect(targetDevices is TargetDevicesWithExtendedWirelessDeviceDiscovery, true); + }); + + testWithoutContext('Ensure factory returns default when not on MacOS', () async { + final BufferLogger logger = BufferLogger.test(); + final Platform platform = FakePlatform(); + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + + final TargetDevices targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); + + expect(targetDevices is TargetDevicesWithExtendedWirelessDeviceDiscovery, false); + }); + + group('findAllTargetDevices on non-MacOS platform', () { late Platform platform; final FakeDevice attachedAndroidDevice1 = FakeDevice(deviceName: 'target-device-1'); @@ -36,7 +74,7 @@ void main() { final FakeDevice exactMatchAndroidDevice = FakeDevice(deviceName: 'target-device'); final FakeDevice exactMatchWirelessAndroidDevice = FakeDevice.wireless(deviceName: 'target-device'); - final FakeDevice exactMatchattachedUnsupportedAndroidDevice = FakeDevice(deviceName: 'target-device', deviceSupported: false); + final FakeDevice exactMatchAttachedUnsupportedAndroidDevice = FakeDevice(deviceName: 'target-device', deviceSupported: false); final FakeDevice exactMatchUnsupportedByProjectDevice = FakeDevice(deviceName: 'target-device', deviceSupportForProject: false); setUp(() { @@ -60,6 +98,7 @@ void main() { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1]; final TargetDevices targetDevices = TargetDevices( + platform: platform, deviceManager: deviceManager, logger: logger, ); @@ -87,6 +126,7 @@ Unable to locate a development device; please run 'flutter doctor' for informati deviceManager.hasSpecifiedAllDevices = true; final TargetDevices targetDevices = TargetDevices( + platform: platform, deviceManager: deviceManager, logger: logger, ); @@ -101,9 +141,34 @@ Unable to locate a development device; please run 'flutter doctor' for informati expect(deviceManager.androidDiscoverer.numberOfTimesPolled, 1); }); + testUsingContext('ensure unsupported for projects are included when includeDevicesUnsupportedByProject is true', () async { + final BufferLogger logger = BufferLogger.test(); + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + deviceManager.androidDiscoverer.deviceList = [attachedUnsupportedAndroidDevice, attachedUnsupportedForProjectAndroidDevice]; + + final TargetDevices targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); + final List? devices = await targetDevices.findAllTargetDevices( + includeDevicesUnsupportedByProject: true, + ); + + expect(logger.statusText, equals('')); + expect(devices, [attachedUnsupportedForProjectAndroidDevice]); + expect(deviceManager.androidDiscoverer.devicesCalled, 2); + expect(deviceManager.androidDiscoverer.discoverDevicesCalled, 0); + expect(deviceManager.androidDiscoverer.numberOfTimesPolled, 1); + }); + group('finds no devices', () { late BufferLogger logger; late TestDeviceManager deviceManager; + late TargetDevices targetDevices; setUp(() { logger = BufferLogger.test(); @@ -111,14 +176,15 @@ Unable to locate a development device; please run 'flutter doctor' for informati logger: logger, platform: platform, ); + targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); }); - group('when device not specified', () { + group('with device not specified', () { testUsingContext('when no devices', () async { - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -136,10 +202,6 @@ No supported devices connected. attachedUnsupportedForProjectAndroidDevice, ]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -163,10 +225,6 @@ If you would like your app to run on android, consider running `flutter create . }); testUsingContext('when no devices', () async { - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -183,10 +241,6 @@ No supported devices found with name or id matching 'target-device'. final FakeDevice device2 = FakeDevice.wireless(deviceName: 'no-match-2'); deviceManager.androidDiscoverer.deviceList = [device1, device2]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -203,12 +257,8 @@ no-match-2 (mobile) • xxx • android • Android 10 }); testUsingContext('when matching device is unsupported by flutter', () async { - deviceManager.androidDiscoverer.deviceList = [exactMatchattachedUnsupportedAndroidDevice]; + deviceManager.androidDiscoverer.deviceList = [exactMatchAttachedUnsupportedAndroidDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -230,10 +280,6 @@ target-device (mobile) • xxx • android • Android 10 (unsupported) }); testUsingContext('when no devices', () async { - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -252,10 +298,6 @@ No devices found. ]; deviceManager.otherDiscoverer.deviceList = [fuchsiaDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -274,12 +316,12 @@ If you would like your app to run on android or fuchsia, consider running `flutt }); }); - }); group('finds single device', () { late BufferLogger logger; late TestDeviceManager deviceManager; + late TargetDevices targetDevices; setUp(() { logger = BufferLogger.test(); @@ -287,16 +329,17 @@ If you would like your app to run on android or fuchsia, consider running `flutt logger: logger, platform: platform, ); + targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); }); - group('when device not specified', () { + group('with device not specified', () { testUsingContext('when single attached device', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -309,10 +352,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when single wireless device', () async { deviceManager.androidDiscoverer.deviceList = [wirelessAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -325,10 +364,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when multiple but only one ephemeral', () async { deviceManager.androidDiscoverer.deviceList = [nonEphemeralDevice, wirelessAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -346,14 +381,10 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when multiple matches but first is unsupported by flutter', () async { deviceManager.androidDiscoverer.deviceList = [ - exactMatchattachedUnsupportedAndroidDevice, + exactMatchAttachedUnsupportedAndroidDevice, exactMatchAndroidDevice, ]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -366,10 +397,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when matching device is unsupported by project', () async { deviceManager.androidDiscoverer.deviceList = [exactMatchUnsupportedByProjectDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -382,10 +409,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when matching attached device', () async { deviceManager.androidDiscoverer.deviceList = [exactMatchAndroidDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -398,10 +421,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when matching wireless device', () async { deviceManager.androidDiscoverer.deviceList = [exactMatchWirelessAndroidDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -411,13 +430,9 @@ If you would like your app to run on android or fuchsia, consider running `flutt expect(deviceManager.androidDiscoverer.numberOfTimesPolled, 1); }); - testUsingContext('when exact match attached device and partial match wireless device', () async { + testUsingContext('when exact matching an attached device and partial matching a wireless device', () async { deviceManager.androidDiscoverer.deviceList = [exactMatchAndroidDevice, wirelessAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -436,10 +451,6 @@ If you would like your app to run on android or fuchsia, consider running `flutt testUsingContext('when only one device', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -452,9 +463,10 @@ If you would like your app to run on android or fuchsia, consider running `flutt }); - group('Finds multiple devices', () { + group('finds multiple devices', () { late BufferLogger logger; late TestDeviceManager deviceManager; + late TargetDevices targetDevices; setUp(() { logger = BufferLogger.test(); @@ -462,9 +474,14 @@ If you would like your app to run on android or fuchsia, consider running `flutt logger: logger, platform: platform, ); + targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); }); - group('when device not specified', () { + group('with device not specified', () { group('with stdinHasTerminal', () { late FakeTerminal terminal; @@ -481,16 +498,12 @@ If you would like your app to run on android or fuchsia, consider running `flutt wirelessUnsupportedAndroidDevice, wirelessUnsupportedForProjectAndroidDevice, ]; - - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); terminal.setPrompt(['1', '2', 'q', 'Q'], '2'); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' -Multiple devices found: +Connected devices: target-device-1 (mobile) • xxx • android • Android 10 Wirelessly connected devices: @@ -509,16 +522,12 @@ target-device-5 (mobile) • xxx • android • Android 10 testUsingContext('including only attached devices', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1, attachedAndroidDevice2]; - - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' -Multiple devices found: +Connected devices: target-device-1 (mobile) • xxx • android • Android 10 target-device-2 (mobile) • xxx • android • Android 10 [1]: target-device-1 (xxx) @@ -534,16 +543,12 @@ target-device-2 (mobile) • xxx • android • Android 10 testUsingContext('including only wireless devices', () async { deviceManager.androidDiscoverer.deviceList = [wirelessAndroidDevice1, wirelessAndroidDevice2]; - - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' -Multiple devices found: +Connected devices: Wirelessly connected devices: target-device-5 (mobile) • xxx • android • Android 10 @@ -559,7 +564,6 @@ target-device-6 (mobile) • xxx • android • Android 10 }, overrides: { AnsiTerminal: () => terminal, }); - }); group('without stdinHasTerminal', () { @@ -579,10 +583,6 @@ target-device-6 (mobile) • xxx • android • Android 10 wirelessUnsupportedForProjectAndroidDevice, ]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -606,10 +606,6 @@ target-device-8 (mobile) • xxx • android • Android 10 testUsingContext('including only attached devices', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1, attachedAndroidDevice2]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -629,10 +625,6 @@ target-device-2 (mobile) • xxx • android • Android 10 testUsingContext('including only wireless devices', () async { deviceManager.androidDiscoverer.deviceList = [wirelessAndroidDevice1, wirelessAndroidDevice2]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -673,12 +665,8 @@ target-device-6 (mobile) • xxx • android • Android 10 wirelessUnsupportedAndroidDevice, wirelessUnsupportedForProjectAndroidDevice, ]; - terminal.setPrompt(['1', '2', '3', '4', 'q', 'Q'], '2'); - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -705,12 +693,8 @@ target-device-8 (mobile) • xxx • android • Android 10 testUsingContext('including only attached devices', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1, attachedAndroidDevice2]; - terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -730,12 +714,8 @@ target-device-2 (mobile) • xxx • android • Android 10 testUsingContext('including only wireless devices', () async { deviceManager.androidDiscoverer.deviceList = [wirelessAndroidDevice1, wirelessAndroidDevice2]; - terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); + final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -767,10 +747,6 @@ target-device-6 (mobile) • xxx • android • Android 10 testUsingContext('including only one ephemeral', () async { deviceManager.androidDiscoverer.deviceList = [nonEphemeralDevice, attachedAndroidDevice1]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -796,10 +772,6 @@ target-device-1 (mobile) • xxx • android • Android 10 wirelessUnsupportedForProjectAndroidDevice, ]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -822,10 +794,6 @@ target-device-8 (mobile) • xxx • android • Android 10 testUsingContext('including only attached devices', () async { deviceManager.androidDiscoverer.deviceList = [attachedAndroidDevice1, attachedAndroidDevice2]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -844,10 +812,6 @@ target-device-2 (mobile) • xxx • android • Android 10 testUsingContext('including only wireless devices', () async { deviceManager.androidDiscoverer.deviceList = [wirelessAndroidDevice1, wirelessAndroidDevice2]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals(''' @@ -883,10 +847,6 @@ target-device-6 (mobile) • xxx • android • Android 10 ]; deviceManager.otherDiscoverer.deviceList = [fuchsiaDevice]; - final TargetDevices targetDevices = TargetDevices( - deviceManager: deviceManager, - logger: logger, - ); final List? devices = await targetDevices.findAllTargetDevices(); expect(logger.statusText, equals('')); @@ -897,8 +857,1389 @@ target-device-6 (mobile) • xxx • android • Android 10 }); }); }); - }); + + group('findAllTargetDevices on mac platform', () { + late Platform platform; + + final FakeIOSDevice attachedIOSDevice1 = FakeIOSDevice(deviceName: 'target-device-1'); + final FakeIOSDevice attachedIOSDevice2 = FakeIOSDevice(deviceName: 'target-device-2'); + final FakeIOSDevice attachedUnsupportedIOSDevice = FakeIOSDevice(deviceName: 'target-device-3', deviceSupported: false); + final FakeIOSDevice attachedUnsupportedForProjectIOSDevice = FakeIOSDevice(deviceName: 'target-device-4', deviceSupportForProject: false); + + final FakeIOSDevice disconnectedWirelessIOSDevice1 = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device-5'); + final FakeIOSDevice connectedWirelessIOSDevice1 = FakeIOSDevice.connectedWireless(deviceName: 'target-device-5'); + final FakeIOSDevice disconnectedWirelessIOSDevice2 = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device-6'); + final FakeIOSDevice connectedWirelessIOSDevice2 = FakeIOSDevice.connectedWireless(deviceName: 'target-device-6'); + final FakeIOSDevice disconnectedWirelessUnsupportedIOSDevice = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device-7', deviceSupported: false); + final FakeIOSDevice connectedWirelessUnsupportedIOSDevice = FakeIOSDevice.connectedWireless(deviceName: 'target-device-7', deviceSupported: false); + final FakeIOSDevice disconnectedWirelessUnsupportedForProjectIOSDevice = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device-8', deviceSupportForProject: false); + final FakeIOSDevice connectedWirelessUnsupportedForProjectIOSDevice = FakeIOSDevice.connectedWireless(deviceName: 'target-device-8', deviceSupportForProject: false); + + final FakeIOSDevice nonEphemeralDevice = FakeIOSDevice(deviceName: 'target-device-9', ephemeral: false); + final FakeDevice fuchsiaDevice = FakeDevice.fuchsia(deviceName: 'target-device-10'); + + final FakeIOSDevice exactMatchAttachedIOSDevice = FakeIOSDevice(deviceName: 'target-device'); + final FakeIOSDevice exactMatchAttachedUnsupportedIOSDevice = FakeIOSDevice(deviceName: 'target-device', deviceSupported: false); + final FakeIOSDevice exactMatchUnsupportedByProjectDevice = FakeIOSDevice(deviceName: 'target-device', deviceSupportForProject: false); + + setUp(() { + platform = FakePlatform(operatingSystem: 'macos'); + }); + + group('when cannot launch anything', () { + late BufferLogger logger; + late FakeDoctor doctor; + + setUp(() { + logger = BufferLogger.test(); + doctor = FakeDoctor(canLaunchAnything: false); + }); + + testUsingContext('does not search for devices', () async { + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1]; + + final TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.errorText, equals(''' +Unable to locate a development device; please run 'flutter doctor' for information about installing additional components. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 0); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 0); + }, overrides: { + Doctor: () => doctor, + }); + }); + + testUsingContext('ensure refresh when deviceDiscoveryTimeout is provided', () async { + final BufferLogger logger = BufferLogger.test(); + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1]; + + final TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + final List? devices = await targetDevices.findAllTargetDevices( + deviceDiscoveryTimeout: const Duration(seconds: 2), + ); + + expect(logger.statusText, equals('')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 1); + }); + + testUsingContext('ensure unsupported for projects are included when includeDevicesUnsupportedByProject is true', () async { + final BufferLogger logger = BufferLogger.test(); + final TestDeviceManager deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + deviceManager.iosDiscoverer.deviceList = [attachedUnsupportedIOSDevice, attachedUnsupportedForProjectIOSDevice]; + + final TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + final List? devices = await targetDevices.findAllTargetDevices( + includeDevicesUnsupportedByProject: true, + ); + + expect(logger.statusText, equals('')); + expect(devices, [attachedUnsupportedForProjectIOSDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + group('finds no devices', () { + late BufferLogger logger; + late TestDeviceManager deviceManager; + late TargetDevices targetDevices; + + setUp(() { + logger = BufferLogger.test(); + deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + targetDevices = TargetDevices( + platform: platform, + deviceManager: deviceManager, + logger: logger, + ); + }); + + group('with device not specified', () { + testUsingContext('when no devices', () async { + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices connected. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when device is unsupported by flutter or project', () async { + deviceManager.iosDiscoverer.deviceList = [ + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices connected. + +The following devices were found, but are not supported by this project: +target-device-3 (mobile) • xxx • ios • iOS 16 (unsupported) +target-device-4 (mobile) • xxx • ios • iOS 16 +target-device-7 (mobile) • xxx • ios • iOS 16 (unsupported) +target-device-8 (mobile) • xxx • ios • iOS 16 +If you would like your app to run on ios, consider running `flutter create .` to generate projects for these platforms. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when all found devices are not connected', () async { + deviceManager.iosDiscoverer.deviceList = [ + disconnectedWirelessIOSDevice1, + disconnectedWirelessIOSDevice2, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + disconnectedWirelessIOSDevice1, + disconnectedWirelessIOSDevice2, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices connected. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + }); + + group('with hasSpecifiedDeviceId', () { + setUp(() { + deviceManager.specifiedDeviceId = 'target-device'; + }); + + testUsingContext('when no devices', () async { + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices found with name or id matching 'target-device'. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when no devices match', () async { + final FakeIOSDevice device1 = FakeIOSDevice(deviceName: 'no-match-1'); + final FakeIOSDevice device2 = FakeIOSDevice.notConnectedWireless(deviceName: 'no-match-2'); + final FakeIOSDevice device2Connected = FakeIOSDevice.connectedWireless(deviceName: 'no-match-2'); + deviceManager.iosDiscoverer.deviceList = [device1, device2]; + deviceManager.iosDiscoverer.refreshDeviceList = [device1,device2Connected]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices found with name or id matching 'target-device'. + +The following devices were found: +no-match-1 (mobile) • xxx • ios • iOS 16 +no-match-2 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when matching device is unsupported by flutter', () async { + deviceManager.iosDiscoverer.deviceList = [exactMatchAttachedUnsupportedIOSDevice]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No supported devices found with name or id matching 'target-device'. + +The following devices were found: +target-device (mobile) • xxx • ios • iOS 16 (unsupported) +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + }); + + group('with hasSpecifiedAllDevices', () { + setUp(() { + deviceManager.hasSpecifiedAllDevices = true; + }); + + testUsingContext('when no devices', () async { + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No devices found. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when devices are either unsupported by flutter or project or all', () async { + deviceManager.otherDiscoverer.deviceList = [fuchsiaDevice]; + deviceManager.iosDiscoverer.deviceList = [ + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +No devices found. + +The following devices were found, but are not supported by this project: +target-device-10 (mobile) • xxx • fuchsia-arm64 • tester +target-device-3 (mobile) • xxx • ios • iOS 16 (unsupported) +target-device-4 (mobile) • xxx • ios • iOS 16 +target-device-7 (mobile) • xxx • ios • iOS 16 (unsupported) +target-device-8 (mobile) • xxx • ios • iOS 16 +If you would like your app to run on fuchsia or ios, consider running `flutter create .` to generate projects for these platforms. +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + }); + }); + + group('finds single device', () { + late TestBufferLogger logger; + late TestDeviceManager deviceManager; + late TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + + setUp(() { + logger = TestBufferLogger.test(); + deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + group('with device not specified', () { + testUsingContext('when single ephemeral attached device', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when single wireless device', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... +''')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when multiple but only one attached ephemeral', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice, attachedIOSDevice1, disconnectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + group('with stdinHasTerminal', () { + late FakeTerminal terminal; + + setUp(() { + terminal = FakeTerminal(supportsColor: true); + logger = TestBufferLogger.test(terminal: terminal); + }); + + testUsingContext('when single non-ephemeral attached device', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice]; + + final TestTargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices = TestTargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '1'; + logger.originalStatusText = ''' +Connected devices: +target-device-9 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-9 (xxx) +'''; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Connected devices: +target-device-9 (mobile) • xxx • ios • iOS 16 + +No wireless devices were found. + +[1]: target-device-9 (xxx) +Please choose one (or "q" to quit): ''')); + expect(devices, [nonEphemeralDevice]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + + group('without stdinHasTerminal', () { + late FakeTerminal terminal; + + setUp(() { + terminal = FakeTerminal(stdinHasTerminal: false); + targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('when single non-ephemeral attached device', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [nonEphemeralDevice]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + }); + + group('with hasSpecifiedDeviceId', () { + setUp(() { + deviceManager.specifiedDeviceId = 'target-device'; + }); + + testUsingContext('when multiple matches but first is unsupported by flutter', () async { + deviceManager.iosDiscoverer.deviceList = [ + exactMatchAttachedUnsupportedIOSDevice, + exactMatchAttachedIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [exactMatchAttachedIOSDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when matching device is unsupported by project', () async { + deviceManager.iosDiscoverer.deviceList = [exactMatchUnsupportedByProjectDevice]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [exactMatchUnsupportedByProjectDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when matching attached device', () async { + deviceManager.iosDiscoverer.deviceList = [exactMatchAttachedIOSDevice, disconnectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [exactMatchAttachedIOSDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when exact matching wireless device', () async { + final FakeIOSDevice exactMatchWirelessDevice = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device'); + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, exactMatchWirelessDevice]; + deviceManager.setDeviceToWaitFor(exactMatchWirelessDevice, DeviceConnectionInterface.wireless); + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Waiting for target-device to connect... +''')); + expect(devices, [exactMatchWirelessDevice]); + expect(devices?.first.isConnected, true); + expect(devices?.first.connectionInterface, DeviceConnectionInterface.wireless); + expect(deviceManager.iosDiscoverer.devicesCalled, 1); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isTrue); + }); + + testUsingContext('when partially matching single wireless devices', () async { + final FakeIOSDevice partialMatchWirelessDevice = FakeIOSDevice.notConnectedWireless(deviceName: 'target-device-1'); + deviceManager.iosDiscoverer.deviceList = [partialMatchWirelessDevice]; + deviceManager.setDeviceToWaitFor(partialMatchWirelessDevice, DeviceConnectionInterface.wireless); + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Waiting for target-device-1 to connect... +''')); + expect(devices, [partialMatchWirelessDevice]); + expect(devices?.first.isConnected, true); + expect(devices?.first.connectionInterface, DeviceConnectionInterface.wireless); + expect(deviceManager.iosDiscoverer.devicesCalled, 1); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isTrue); + }); + + testUsingContext('when exact matching an attached device and partial matching a wireless device', () async { + deviceManager.iosDiscoverer.deviceList = [exactMatchAttachedIOSDevice, connectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [exactMatchAttachedIOSDevice, connectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [exactMatchAttachedIOSDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when partially matching multiple device but only one is connected', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [attachedIOSDevice1, disconnectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when partially matching single attached device', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when partially matching wireless device and an attached device from different discoverer', () async { + final FakeDevice androidDevice = FakeDevice(deviceName: 'target-device-android'); + deviceManager.androidDiscoverer.deviceList = [androidDevice]; + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [disconnectedWirelessIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [androidDevice]); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }); + + testUsingContext('when matching single non-ephemeral attached device', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals('')); + expect(devices, [nonEphemeralDevice]); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + }); + + group('with hasSpecifiedAllDevices', () { + setUp(() { + deviceManager.hasSpecifiedAllDevices = true; + }); + + testUsingContext('when only one device', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('when single non-ephemeral attached device', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [nonEphemeralDevice]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + }); + + }); + + group('finds multiple devices', () { + late TestBufferLogger logger; + late TestDeviceManager deviceManager; + + setUp(() { + logger = TestBufferLogger.test(); + deviceManager = TestDeviceManager( + logger: logger, + platform: platform, + ); + }); + + group('with device not specified', () { + group('with stdinHasTerminal', () { + late FakeTerminal terminal; + late TestTargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + + setUp(() { + terminal = FakeTerminal(supportsColor: true); + logger = TestBufferLogger.test(terminal: terminal); + targetDevices = TestTargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including attached, wireless, unsupported devices', () async { + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedIOSDevice2, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessIOSDevice1, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + attachedIOSDevice1, + attachedIOSDevice2, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessIOSDevice1, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '3'; + logger.originalStatusText = ''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +'''; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +[3]: target-device-5 (xxx) +Please choose one (or "q" to quit): ''')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '2'; + logger.originalStatusText = ''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +'''; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +No wireless devices were found. + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +Please choose one (or "q" to quit): ''')); + expect(devices, [attachedIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1, disconnectedWirelessIOSDevice2]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '2'; + terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +Connected devices: + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-6 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-5 (xxx) +[2]: target-device-6 (xxx) +''')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + group('but no color support', () { + setUp(() { + terminal = FakeTerminal(); + logger = TestBufferLogger.test(terminal: terminal); + targetDevices = TestTargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('and waits for wireless devices to return', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2, disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [attachedIOSDevice1, attachedIOSDevice2, connectedWirelessIOSDevice1]; + + terminal.setPrompt(['1', '2', '3', 'q', 'Q'], '1'); + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +[3]: target-device-5 (xxx) +''')); + expect(devices, [attachedIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + + group('with verbose logging', () { + setUp(() { + logger = TestBufferLogger.test(terminal: terminal, verbose: true); + targetDevices = TestTargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '2'; + logger.originalStatusText = ''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +'''; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +No wireless devices were found. +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +Please choose one (or "q" to quit): ''')); + + expect(devices, [attachedIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including attached and wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2, disconnectedWirelessIOSDevice1]; + deviceManager.iosDiscoverer.refreshDeviceList = [attachedIOSDevice1, attachedIOSDevice2, connectedWirelessIOSDevice1]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '2'; + logger.originalStatusText = ''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +'''; + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +Connected devices: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +[3]: target-device-5 (xxx) +Please choose one (or "q" to quit): ''')); + + expect(devices, [attachedIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + }); + + group('without stdinHasTerminal', () { + late FakeTerminal terminal; + late TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + + setUp(() { + terminal = FakeTerminal(stdinHasTerminal: false); + targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including attached, wireless, unsupported devices', () async { + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedIOSDevice2, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessIOSDevice1, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedIOSDevice2, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessIOSDevice1, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +More than one device connected; please specify a device with the '-d ' flag, or use '-d all' to act on all devices. + +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 +target-device-4 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-8 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +More than one device connected; please specify a device with the '-d ' flag, or use '-d all' to act on all devices. + +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1, disconnectedWirelessIOSDevice2]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +More than one device connected; please specify a device with the '-d ' flag, or use '-d all' to act on all devices. + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-6 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 4); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + }); + + group('with hasSpecifiedDeviceId', () { + setUp(() { + deviceManager.specifiedDeviceId = 'target-device'; + }); + + group('with stdinHasTerminal', () { + late FakeTerminal terminal; + late TestTargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + + setUp(() { + terminal = FakeTerminal(supportsColor: true); + logger = TestBufferLogger.test(terminal: terminal); + targetDevices = TestTargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including attached, wireless, unsupported devices', () async { + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessIOSDevice1, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessIOSDevice1, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '3'; + logger.originalStatusText = ''' +Found multiple devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-4 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-4 (xxx) +'''; + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Found multiple devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-4 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-8 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-1 (xxx) +[2]: target-device-4 (xxx) +[3]: target-device-5 (xxx) +[4]: target-device-8 (xxx) +Please choose one (or "q" to quit): ''')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + targetDevices.waitForWirelessBeforeInput = true; + targetDevices.deviceSelection.input = '2'; + logger.originalStatusText = ''' +Found multiple devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +Checking for wireless devices... + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +'''; + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Found multiple devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 + +No wireless devices were found. + +[1]: target-device-1 (xxx) +[2]: target-device-2 (xxx) +Please choose one (or "q" to quit): ''')); + expect(devices, [attachedIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1, disconnectedWirelessIOSDevice2]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]; + + terminal.setPrompt(['1', '2', 'q', 'Q'], '1'); + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +Found 2 devices with name or id matching target-device: + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-6 (mobile) • xxx • ios • iOS 16 + +[1]: target-device-5 (xxx) +[2]: target-device-6 (xxx) +''')); + expect(devices, [connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + + group('without stdinHasTerminal', () { + late FakeTerminal terminal; + late TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + + setUp(() { + terminal = FakeTerminal(stdinHasTerminal: false); + targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including only one ephemeral', () async { + deviceManager.iosDiscoverer.deviceList = [nonEphemeralDevice, attachedIOSDevice1]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +Found 2 devices with name or id matching target-device: +target-device-9 (mobile) • xxx • ios • iOS 16 +target-device-1 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including matching attached, wireless, unsupported devices', () async { + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessIOSDevice1, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.refreshDeviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessIOSDevice1, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +Found 4 devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-4 (mobile) • xxx • ios • iOS 16 + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-8 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... + +Found 2 devices with name or id matching target-device: +target-device-1 (mobile) • xxx • ios • iOS 16 +target-device-2 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + + testUsingContext('including only wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1, disconnectedWirelessIOSDevice2]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... + +Found 2 devices with name or id matching target-device: + +Wirelessly connected devices: +target-device-5 (mobile) • xxx • ios • iOS 16 +target-device-6 (mobile) • xxx • ios • iOS 16 +''')); + expect(devices, isNull); + expect(deviceManager.iosDiscoverer.devicesCalled, 3); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + expect(deviceManager.iosDiscoverer.xcdevice.waitedForDeviceToConnect, isFalse); + }, overrides: { + AnsiTerminal: () => terminal, + }); + }); + }); + + group('with hasSpecifiedAllDevices', () { + late TargetDevicesWithExtendedWirelessDeviceDiscovery targetDevices; + setUp(() { + deviceManager.hasSpecifiedAllDevices = true; + targetDevices = TargetDevicesWithExtendedWirelessDeviceDiscovery( + deviceManager: deviceManager, + logger: logger, + ); + }); + + testUsingContext('including attached, wireless, unsupported devices', () async { + deviceManager.otherDiscoverer.deviceList = [fuchsiaDevice]; + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + disconnectedWirelessIOSDevice1, + disconnectedWirelessUnsupportedIOSDevice, + disconnectedWirelessUnsupportedForProjectIOSDevice, + ]; + deviceManager.iosDiscoverer.deviceList = [ + attachedIOSDevice1, + attachedUnsupportedIOSDevice, + attachedUnsupportedForProjectIOSDevice, + connectedWirelessIOSDevice1, + connectedWirelessUnsupportedIOSDevice, + connectedWirelessUnsupportedForProjectIOSDevice, + ]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [attachedIOSDevice1, connectedWirelessIOSDevice1]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('including only attached devices', () async { + deviceManager.iosDiscoverer.deviceList = [attachedIOSDevice1, attachedIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +Checking for wireless devices... +''')); + expect(devices, [attachedIOSDevice1, attachedIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + + testUsingContext('including only wireless devices', () async { + deviceManager.iosDiscoverer.deviceList = [disconnectedWirelessIOSDevice1, disconnectedWirelessIOSDevice2]; + deviceManager.iosDiscoverer.refreshDeviceList = [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]; + + final List? devices = await targetDevices.findAllTargetDevices(); + + expect(logger.statusText, equals(''' +No devices found yet. Checking for wireless devices... +''')); + expect(devices, [connectedWirelessIOSDevice1, connectedWirelessIOSDevice2]); + expect(deviceManager.iosDiscoverer.devicesCalled, 2); + expect(deviceManager.iosDiscoverer.discoverDevicesCalled, 1); + expect(deviceManager.iosDiscoverer.numberOfTimesPolled, 2); + }); + }); + }); + }); +} + +class TestTargetDevicesWithExtendedWirelessDeviceDiscovery extends TargetDevicesWithExtendedWirelessDeviceDiscovery { + TestTargetDevicesWithExtendedWirelessDeviceDiscovery({ + required super.deviceManager, + required super.logger, + }) : _deviceSelection = TestTargetDeviceSelection(logger); + + final TestTargetDeviceSelection _deviceSelection; + + @override + TestTargetDeviceSelection get deviceSelection => _deviceSelection; +} + +class TestTargetDeviceSelection extends TargetDeviceSelection { + TestTargetDeviceSelection(super.logger); + + String input = ''; + + @override + Future readUserInput() async { + return input; + } } class TestDeviceManager extends DeviceManager { @@ -922,8 +2263,11 @@ class TestDeviceManager extends DeviceManager { final TestPollingDeviceDiscovery otherDiscoverer = TestPollingDeviceDiscovery( 'other', ); - final TestPollingDeviceDiscovery iosDiscoverer = TestPollingDeviceDiscovery( - 'ios', + late final TestIOSDeviceDiscovery iosDiscoverer = TestIOSDeviceDiscovery( + platform: platform, + xcdevice: FakeXcdevice(), + iosWorkflow: FakeIOSWorkflow(), + logger: logger, ); @override @@ -934,6 +2278,21 @@ class TestDeviceManager extends DeviceManager { iosDiscoverer, ]; } + + void setDeviceToWaitFor( + IOSDevice device, + DeviceConnectionInterface connectionInterface, + ) { + final XCDeviceEventInterface eventInterface = + connectionInterface == DeviceConnectionInterface.wireless + ? XCDeviceEventInterface.wifi + : XCDeviceEventInterface.usb; + iosDiscoverer.xcdevice.waitForDeviceEvent = XCDeviceEventNotification( + XCDeviceEvent.attach, + eventInterface, + device.id, + ); + } } class TestPollingDeviceDiscovery extends PollingDeviceDiscovery { @@ -979,6 +2338,83 @@ class TestPollingDeviceDiscovery extends PollingDeviceDiscovery { bool get canListAnything => true; } +class TestIOSDeviceDiscovery extends IOSDevices { + TestIOSDeviceDiscovery({ + required super.platform, + required FakeXcdevice xcdevice, + required super.iosWorkflow, + required super.logger, + }) : _platform = platform, + _xcdevice = xcdevice, + super(xcdevice: xcdevice); + + final Platform _platform; + List deviceList = []; + List refreshDeviceList = []; + int devicesCalled = 0; + int discoverDevicesCalled = 0; + int numberOfTimesPolled = 0; + + final FakeXcdevice _xcdevice; + + @override + FakeXcdevice get xcdevice => _xcdevice; + + @override + Future> pollingGetDevices({Duration? timeout}) async { + numberOfTimesPolled++; + if (!_platform.isMacOS) { + throw UnsupportedError( + 'Control of iOS devices or simulators only supported on macOS.', + ); + } + return deviceList; + } + + @override + Future> devices({DeviceDiscoveryFilter? filter}) async { + devicesCalled += 1; + return super.devices(filter: filter); + } + + @override + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) { + discoverDevicesCalled++; + if (refreshDeviceList.isNotEmpty) { + deviceList = refreshDeviceList; + } + return super.discoverDevices(timeout: timeout, filter: filter); + } + + @override + bool get canListAnything => true; +} + +class FakeXcdevice extends Fake implements XCDevice { + XCDeviceEventNotification? waitForDeviceEvent; + + bool waitedForDeviceToConnect = false; + + @override + Future waitForDeviceToConnect(String deviceId) async { + final XCDeviceEventNotification? waitEvent = waitForDeviceEvent; + if (waitEvent != null) { + waitedForDeviceToConnect = true; + return XCDeviceEventNotification(waitEvent.eventType, waitEvent.eventInterface, waitEvent.deviceIdentifier); + } else { + return null; + } + } + + @override + void cancelWaitForDeviceToConnect() {} +} + +class FakeIOSWorkflow extends Fake implements IOSWorkflow {} + // Unfortunately Device, despite not being immutable, has an `operator ==`. // Until we fix that, we have to also ignore related lints here. // ignore: avoid_implementing_value_types @@ -1078,15 +2514,113 @@ class FakeDevice extends Fake implements Device { getNameForTargetPlatform(await targetPlatform); } +// Unfortunately Device, despite not being immutable, has an `operator ==`. +// Until we fix that, we have to also ignore related lints here. +// ignore: avoid_implementing_value_types +class FakeIOSDevice extends Fake implements IOSDevice { + FakeIOSDevice({ + String? deviceId, + String? deviceName, + bool deviceSupported = true, + bool deviceSupportForProject = true, + this.ephemeral = true, + this.isConnected = true, + this.platformType = PlatformType.ios, + this.connectionInterface = DeviceConnectionInterface.attached, + }) : id = deviceId ?? 'xxx', + name = deviceName ?? 'test', + _isSupported = deviceSupported, + _isSupportedForProject = deviceSupportForProject; + + FakeIOSDevice.notConnectedWireless({ + String? deviceId, + String? deviceName, + bool deviceSupported = true, + bool deviceSupportForProject = true, + this.ephemeral = true, + this.isConnected = false, + this.platformType = PlatformType.ios, + this.connectionInterface = DeviceConnectionInterface.wireless, + }) : id = deviceId ?? 'xxx', + name = deviceName ?? 'test', + _isSupported = deviceSupported, + _isSupportedForProject = deviceSupportForProject; + + FakeIOSDevice.connectedWireless({ + String? deviceId, + String? deviceName, + bool deviceSupported = true, + bool deviceSupportForProject = true, + this.ephemeral = true, + this.isConnected = true, + this.platformType = PlatformType.ios, + this.connectionInterface = DeviceConnectionInterface.wireless, + }) : id = deviceId ?? 'xxx', + name = deviceName ?? 'test', + _isSupported = deviceSupported, + _isSupportedForProject = deviceSupportForProject; + + final bool _isSupported; + final bool _isSupportedForProject; + + @override + String name; + + @override + final bool ephemeral; + + @override + String id; + + @override + bool isSupported() => _isSupported; + + @override + bool isSupportedForProject(FlutterProject project) => _isSupportedForProject; + + @override + DeviceConnectionInterface connectionInterface; + + @override + bool isConnected; + + @override + final PlatformType? platformType; + + @override + Future get sdkNameAndVersion async => 'iOS 16'; + + @override + Future get isLocalEmulator async => false; + + @override + Category? get category => Category.mobile; + + @override + Future get targetPlatformDisplayName async => 'ios'; + + @override + Future get targetPlatform async => TargetPlatform.tester; +} + class FakeTerminal extends Fake implements AnsiTerminal { - FakeTerminal({this.stdinHasTerminal = true}); + FakeTerminal({ + this.stdinHasTerminal = true, + this.supportsColor = false, + }); @override final bool stdinHasTerminal; + @override + final bool supportsColor; + @override bool usesTerminalUi = true; + @override + bool singleCharMode = false; + void setPrompt(List characters, String result) { _nextPrompt = characters; _nextResult = result; @@ -1106,6 +2640,54 @@ class FakeTerminal extends Fake implements AnsiTerminal { expect(acceptedCharacters, _nextPrompt); return _nextResult; } + + @override + String clearLines(int numberOfLines) { + return 'CLEAR_LINES_$numberOfLines'; + } +} + +class TestBufferLogger extends BufferLogger { + TestBufferLogger.test({ + 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 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, + ); + } + } } class FakeDoctor extends Fake implements Doctor { diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 8d8358f14e1..d70c3a44d51 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -218,10 +218,17 @@ class FakeDeviceManager implements DeviceManager { DeviceDiscoveryFilter? filter, }) async => filteredDevices(filter); + @override + Future> refreshExtendedWirelessDeviceDiscoverers({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async => filteredDevices(filter); + @override Future> 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> getDevices({ DeviceDiscoveryFilter? filter, + bool waitForDeviceToConnect = false, }) { return hasSpecifiedDeviceId ? getDevicesById(specifiedDeviceId!, filter: filter) diff --git a/packages/flutter_tools/test/src/fake_devices.dart b/packages/flutter_tools/test/src/fake_devices.dart index c74f7adb647..0b3529add59 100644 --- a/packages/flutter_tools/test/src/fake_devices.dart +++ b/packages/flutter_tools/test/src/fake_devices.dart @@ -79,6 +79,33 @@ List fakeDevices = [ }, } ), + FakeDeviceJsonData( + FakeDevice( + 'wireless ios', + 'wireless-ios', + type:PlatformType.ios, + connectionInterface: DeviceConnectionInterface.wireless, + ) + ..targetPlatform = Future.value(TargetPlatform.ios) + ..sdkNameAndVersion = Future.value('iOS 16'), + { + 'name': 'wireless ios', + 'id': 'wireless-ios', + 'isSupported': true, + 'targetPlatform': 'ios', + 'emulator': true, + 'sdk': 'iOS 16', + 'capabilities': { + '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 _devices = []; final StreamController _onAddedController = StreamController.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);