From cc26a1aa0cb793adacfa556fb6495e023d4501c8 Mon Sep 17 00:00:00 2001 From: Victoria Ashworth Date: Fri, 3 Mar 2023 12:06:16 -0600 Subject: [PATCH] Update device filtering and introduce isConnected and connectionInterface (#121359) Update device filtering and introduce isConnected and connectionInterface --- .../lib/src/commands/daemon.dart | 4 +- .../lib/src/commands/devices.dart | 2 +- .../flutter_tools/lib/src/commands/drive.dart | 4 +- .../flutter_tools/lib/src/commands/logs.dart | 2 +- packages/flutter_tools/lib/src/device.dart | 331 +++++++++++--- packages/flutter_tools/lib/src/doctor.dart | 2 +- .../lib/src/proxied_devices/devices.dart | 9 +- .../lib/src/runner/flutter_command.dart | 18 +- .../commands.shard/hermetic/attach_test.dart | 6 + .../commands.shard/hermetic/devices_test.dart | 21 +- .../commands.shard/hermetic/doctor_test.dart | 4 +- .../commands.shard/hermetic/drive_test.dart | 10 +- .../hermetic/proxied_devices_test.dart | 8 +- .../commands.shard/hermetic/run_test.dart | 3 + .../commands.shard/hermetic/test_test.dart | 4 +- .../permeable/devices_test.dart | 5 +- .../custom_devices/custom_device_test.dart | 6 +- .../test/general.shard/device_test.dart | 421 +++++++++++++++--- .../linux/linux_device_test.dart | 6 +- .../macos/macos_device_test.dart | 6 +- .../macos/macos_ipad_device_test.dart | 8 +- .../tester/flutter_tester_test.dart | 4 +- .../windows/windows_device_test.dart | 4 +- packages/flutter_tools/test/src/context.dart | 41 +- .../flutter_tools/test/src/fake_devices.dart | 13 +- 25 files changed, 767 insertions(+), 175 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 559880e88c..8716a0e9ae 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -883,7 +883,7 @@ class DeviceDomain extends Domain { 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()) await _deviceToMap(device), ]; } @@ -1069,7 +1069,7 @@ class DeviceDomain extends Domain { /// Return the 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(); 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 afd931843d..c7ceff1ed1 100644 --- a/packages/flutter_tools/lib/src/commands/devices.dart +++ b/packages/flutter_tools/lib/src/commands/devices.dart @@ -62,7 +62,7 @@ class DevicesCommand extends FlutterCommand { exitCode: 1); } - final List devices = await globals.deviceManager?.refreshAllConnectedDevices(timeout: deviceDiscoveryTimeout) ?? []; + final List devices = await globals.deviceManager?.refreshAllDevices(timeout: deviceDiscoveryTimeout) ?? []; if (boolArgDeprecated('machine')) { await printDevicesAsJson(devices); diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 379009ac0e..9c1f8a7e9b 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -209,7 +209,9 @@ class DriveCommand extends RunCommandBase { String? get applicationBinaryPath => stringArgDeprecated(FlutterOptions.kUseApplicationBinary); Future get targetedDevice async { - return findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null); + return findTargetDevice( + includeDevicesUnsupportedByProject: applicationBinaryPath == null, + ); } // Network devices need `publish-port` to be enabled because it requires mDNS. diff --git a/packages/flutter_tools/lib/src/commands/logs.dart b/packages/flutter_tools/lib/src/commands/logs.dart index 71dfb235d2..1acce6388c 100644 --- a/packages/flutter_tools/lib/src/commands/logs.dart +++ b/packages/flutter_tools/lib/src/commands/logs.dart @@ -36,7 +36,7 @@ class LogsCommand extends FlutterCommand { @override Future verifyThenRunCommand(String? commandPath) async { - device = await findTargetDevice(includeUnsupportedDevices: true); + device = await findTargetDevice(includeDevicesUnsupportedByProject: true); if (device == null) { throwToolExit(null); } diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 8d4b8c9f84..deb3761fa0 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -110,7 +110,19 @@ abstract class DeviceManager { /// specifiedDeviceId = 'all'. bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all'; - Future> getDevicesById(String deviceId) async { + /// Get devices filtered by [filter] that match the given device id/name. + /// + /// If [filter] is not provided, a default filter that requires devices to be + /// connected will be used. + /// + /// If an exact match is found, return it immediately. Otherwise wait for all + /// discoverers to complete and return any partial matches. + Future> getDevicesById( + String deviceId, { + DeviceDiscoveryFilter? filter, + }) async { + filter ??= DeviceDiscoveryFilter(); + final String lowerDeviceId = deviceId.toLowerCase(); bool exactlyMatchesDeviceId(Device device) => device.id.toLowerCase() == lowerDeviceId || @@ -135,7 +147,7 @@ abstract class DeviceManager { for (final DeviceDiscovery discoverer in _platformDiscoverers) if (!hasWellKnownId || discoverer.wellKnownIds.contains(specifiedDeviceId)) discoverer - .devices + .devices(filter: filter) .then((List devices) { for (final Device device in devices) { if (exactlyMatchesDeviceId(device)) { @@ -165,34 +177,56 @@ abstract class DeviceManager { return prefixMatches; } - /// Returns the list of connected devices, filtered by any user-specified device id. - Future> getDevices() { + /// Returns a list of devices filtered by the user-specified device + /// id/name (if applicable) and [filter]. + /// + /// If [filter] is not provided, a default filter that requires devices to be + /// connected will be used. + Future> getDevices({ + DeviceDiscoveryFilter? filter, + }) { + filter ??= DeviceDiscoveryFilter(); final String? id = specifiedDeviceId; if (id == null) { - return getAllConnectedDevices(); + return getAllDevices(filter: filter); } - return getDevicesById(id); + return getDevicesById(id, filter: filter); } Iterable get _platformDiscoverers { return deviceDiscoverers.where((DeviceDiscovery discoverer) => discoverer.supportsPlatform); } - /// Returns the list of all connected devices. - Future> getAllConnectedDevices() async { + /// Returns a list of devices filtered by [filter]. + /// + /// If [filter] is not provided, a default filter that requires devices to be + /// connected will be used. + Future> getAllDevices({ + DeviceDiscoveryFilter? filter, + }) async { + filter ??= DeviceDiscoveryFilter(); final List> devices = await Future.wait>(>>[ for (final DeviceDiscovery discoverer in _platformDiscoverers) - discoverer.devices, + discoverer.devices(filter: filter), ]); return devices.expand((List deviceList) => deviceList).toList(); } - /// Returns the list of all connected devices. Discards existing cache of devices. - Future> refreshAllConnectedDevices({ Duration? timeout }) async { + /// Returns a list of devices filtered by [filter]. Discards existing cache of devices. + /// + /// If [filter] is not provided, a default filter that requires devices to be + /// connected will be used. + /// + /// Search for devices to populate the cache for no longer than [timeout]. + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async { + filter ??= DeviceDiscoveryFilter(); final List> devices = await Future.wait>(>>[ for (final DeviceDiscovery discoverer in _platformDiscoverers) - discoverer.discoverDevices(timeout: timeout), + discoverer.discoverDevices(filter: filter, timeout: timeout), ]); return devices.expand((List deviceList) => deviceList).toList(); @@ -233,45 +267,26 @@ abstract class DeviceManager { /// * If [promptUserToChooseDevice] is true, and there are more than one /// device after the aforementioned filters, and the user is connected to a /// terminal, then show a prompt asking the user to choose one. - Future> findTargetDevices( - FlutterProject? flutterProject, { + Future> findTargetDevices({ + bool includeDevicesUnsupportedByProject = false, Duration? timeout, }) async { if (timeout != null) { // Reset the cache with the specified timeout. - await refreshAllConnectedDevices(timeout: timeout); + await refreshAllDevices(timeout: timeout); } - List devices = (await getDevices()) - .where((Device device) => device.isSupported()).toList(); + final List devices = await getDevices( + filter: DeviceDiscoveryFilter( + supportFilter: deviceSupportFilter( + includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject, + ), + ), + ); - if (hasSpecifiedAllDevices) { - // User has specified `--device all`. - // - // Always remove web and fuchsia devices from `--all`. This setting - // currently requires devices to share a frontend_server and resident - // runner instance. Both web and fuchsia require differently configured - // compilers, and web requires an entirely different resident runner. - devices = [ - for (final Device device in devices) - if (await device.targetPlatform != TargetPlatform.fuchsia_arm64 && - await device.targetPlatform != TargetPlatform.fuchsia_x64 && - await device.targetPlatform != TargetPlatform.web_javascript && - isDeviceSupportedForProject(device, flutterProject)) - device, - ]; - } else if (!hasSpecifiedDeviceId) { + if (!hasSpecifiedDeviceId) { // User did not specify the device. - // Remove all devices which are not supported by the current application. - // For example, if there was no 'android' folder then don't attempt to - // launch with an Android device. - devices = [ - for (final Device device in devices) - if (isDeviceSupportedForProject(device, flutterProject)) - device, - ]; - if (devices.length > 1) { // If there are still multiple devices and the user did not specify to run // all, then attempt to prioritize ephemeral devices. For example, if the @@ -287,7 +302,7 @@ abstract class DeviceManager { ]; if (ephemeralDevices.length == 1) { - devices = ephemeralDevices; + return ephemeralDevices; } } } @@ -295,18 +310,166 @@ abstract class DeviceManager { return devices; } - /// Returns whether the device is supported for the project. + /// Determines how to filter devices. /// - /// This exists to allow the check to be overridden for google3 clients. If - /// [flutterProject] is null then return true. - bool isDeviceSupportedForProject(Device device, FlutterProject? flutterProject) { - if (flutterProject == null) { - return true; + /// By default, filters to only include devices that are supported by Flutter. + /// + /// If the user has not specificied a device, filters to only include devices + /// that are supported by Flutter and supported by the project. + /// + /// If the user has specified `--device all`, filters to only include devices + /// that are supported by Flutter, supported by the project, and supported for `all`. + /// + /// If [includeDevicesUnsupportedByProject] is true, all devices will be + /// considered supported by the project, regardless of user specifications. + /// + /// This also exists to allow the check to be overridden for google3 clients. + DeviceDiscoverySupportFilter deviceSupportFilter({ + bool includeDevicesUnsupportedByProject = false, + }) { + FlutterProject? flutterProject; + if (includeDevicesUnsupportedByProject == false) { + flutterProject = FlutterProject.current(); + } + if (hasSpecifiedAllDevices) { + return DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProjectOrAll( + flutterProject: flutterProject, + ); + } else if (!hasSpecifiedDeviceId) { + return DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject( + flutterProject: flutterProject, + ); + } else { + return DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutter(); } - return device.isSupportedForProject(flutterProject); } } +/// A class for determining how to filter devices based on if they are supported. +class DeviceDiscoverySupportFilter { + /// Filter devices to only include those supported by Flutter. + DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutter() + : _excludeDevicesNotSupportedByProject = false, + _excludeDevicesNotSupportedByAll = false, + _flutterProject = null; + + /// Filter devices to only include those supported by Flutter and the + /// provided [flutterProject]. + /// + /// If [flutterProject] is null, all devices will be considered supported by + /// the project. + DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject({ + required FlutterProject? flutterProject, + }) : _flutterProject = flutterProject, + _excludeDevicesNotSupportedByProject = true, + _excludeDevicesNotSupportedByAll = false; + + /// Filter devices to only include those supported by Flutter, the provided + /// [flutterProject], and `--device all`. + /// + /// If [flutterProject] is null, all devices will be considered supported by + /// the project. + DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProjectOrAll({ + required FlutterProject? flutterProject, + }) : _flutterProject = flutterProject, + _excludeDevicesNotSupportedByProject = true, + _excludeDevicesNotSupportedByAll = true; + + final FlutterProject? _flutterProject; + final bool _excludeDevicesNotSupportedByProject; + final bool _excludeDevicesNotSupportedByAll; + + Future matchesRequirements(Device device) async { + final bool meetsSupportByFlutterRequirement = device.isSupported(); + final bool meetsSupportForProjectRequirement = !_excludeDevicesNotSupportedByProject || isDeviceSupportedForProject(device); + final bool meetsSupportForAllRequirement = !_excludeDevicesNotSupportedByAll || await isDeviceSupportedForAll(device); + + return meetsSupportByFlutterRequirement && + meetsSupportForProjectRequirement && + meetsSupportForAllRequirement; + } + + /// User has specified `--device all`. + /// + /// Always remove web and fuchsia devices from `all`. This setting + /// currently requires devices to share a frontend_server and resident + /// runner instance. Both web and fuchsia require differently configured + /// compilers, and web requires an entirely different resident runner. + Future isDeviceSupportedForAll(Device device) async { + final TargetPlatform devicePlatform = await device.targetPlatform; + return device.isSupported() && + devicePlatform != TargetPlatform.fuchsia_arm64 && + devicePlatform != TargetPlatform.fuchsia_x64 && + devicePlatform != TargetPlatform.web_javascript && + isDeviceSupportedForProject(device); + } + + /// Returns whether the device is supported for the project. + /// + /// A device can be supported by Flutter but not supported for the project + /// (e.g. when the user has removed the iOS directory from their project). + /// + /// This also exists to allow the check to be overridden for google3 clients. If + /// [_flutterProject] is null then return true. + bool isDeviceSupportedForProject(Device device) { + if (!device.isSupported()) { + return false; + } + if (_flutterProject == null) { + return true; + } + return device.isSupportedForProject(_flutterProject!); + } +} + +/// A class for filtering devices. +/// +/// If [excludeDisconnected] is true, only devices detected as connected will be included. +/// +/// If [supportFilter] is provided, only devices matching the requirements will be included. +/// +/// If [deviceConnectionInterface] is provided, only devices matching the DeviceConnectionInterface will be included. +class DeviceDiscoveryFilter { + DeviceDiscoveryFilter({ + this.excludeDisconnected = true, + this.supportFilter, + this.deviceConnectionInterface, + }); + + final bool excludeDisconnected; + final DeviceDiscoverySupportFilter? supportFilter; + final DeviceConnectionInterface? deviceConnectionInterface; + + Future matchesRequirements(Device device) async { + final DeviceDiscoverySupportFilter? localSupportFilter = supportFilter; + + final bool meetsConnectionRequirement = !excludeDisconnected || device.isConnected; + final bool meetsSupportRequirements = localSupportFilter == null || (await localSupportFilter.matchesRequirements(device)); + final bool meetsConnectionInterfaceRequirement = matchesDeviceConnectionInterface(device, deviceConnectionInterface); + + return meetsConnectionRequirement && + meetsSupportRequirements && + meetsConnectionInterfaceRequirement; + } + + Future> filterDevices(List devices) async { + devices = [ + for (final Device device in devices) + if (await matchesRequirements(device)) device, + ]; + return devices; + } + + bool matchesDeviceConnectionInterface( + Device device, + DeviceConnectionInterface? deviceConnectionInterface, + ) { + if (deviceConnectionInterface == null) { + return true; + } + return device.connectionInterface == deviceConnectionInterface; + } +} /// An abstract class to discover and enumerate a specific type of devices. abstract class DeviceDiscovery { @@ -317,10 +480,13 @@ abstract class DeviceDiscovery { bool get canListAnything; /// Return all connected devices, cached on subsequent calls. - Future> get devices; + Future> devices({DeviceDiscoveryFilter? filter}); /// Return all connected devices. Discards existing cache of devices. - Future> discoverDevices({ Duration? timeout }); + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }); /// Gets a list of diagnostic messages pertaining to issues with any connected /// devices (will be an empty list if there are no issues). @@ -352,7 +518,7 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { Timer? _timer; - Future> pollingGetDevices({ Duration? timeout }); + Future> pollingGetDevices({Duration? timeout}); void startPolling() { if (_timer == null) { @@ -380,19 +546,50 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { _timer = null; } + /// Get devices from cache filtered by [filter]. + /// + /// If the cache is empty, populate the cache. @override - Future> get devices { - return _populateDevices(); + Future> devices({DeviceDiscoveryFilter? filter}) { + return _populateDevices(filter: filter); } + /// 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]. @override - Future> discoverDevices({ Duration? timeout }) { - deviceNotifier = null; - return _populateDevices(timeout: timeout); + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) { + return _populateDevices(timeout: timeout, filter: filter, resetCache: true); } - Future> _populateDevices({ Duration? timeout }) async { - deviceNotifier ??= ItemListNotifier.from(await pollingGetDevices(timeout: timeout)); + /// Get devices from cache filtered by [filter]. + /// + /// If the cache is empty or [resetCache] is true, populate the cache. + /// + /// Search for devices to populate the cache for no longer than [timeout]. + Future> _populateDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + bool resetCache = false, + }) async { + if (deviceNotifier == null || resetCache) { + final List devices = await pollingGetDevices(timeout: timeout); + // If the cache was populated while the polling was ongoing, do not + // overwrite the cache unless it's explicitly refreshing the cache. + if (resetCache) { + deviceNotifier = ItemListNotifier.from(devices); + } else { + deviceNotifier ??= ItemListNotifier.from(devices); + } + } + + // If a filter is provided, filter cache to only return devices matching. + if (filter != null) { + return filter.filterDevices(deviceNotifier!.items); + } return deviceNotifier!.items; } @@ -412,6 +609,12 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { String toString() => '$name device discovery'; } +/// How a device is connected. +enum DeviceConnectionInterface { + attached, + wireless, +} + /// A device is a physical hardware that can run a Flutter application. /// /// This may correspond to a connected iOS or Android device, or represent @@ -434,6 +637,14 @@ abstract class Device { /// Whether this is an ephemeral device. final bool ephemeral; + bool get isConnected => true; + + DeviceConnectionInterface get connectionInterface => + DeviceConnectionInterface.attached; + + bool get isWirelesslyConnected => + connectionInterface == DeviceConnectionInterface.wireless; + String get name; bool get supportsStartPaused => true; diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index c9a608366d..a10a301754 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -688,7 +688,7 @@ class DeviceValidator extends DoctorValidator { @override Future validate() async { - final List devices = await _deviceManager.getAllConnectedDevices(); + final List devices = await _deviceManager.getAllDevices(); List installedMessages = []; if (devices.isNotEmpty) { installedMessages = (await Device.descriptions(devices)) diff --git a/packages/flutter_tools/lib/src/proxied_devices/devices.dart b/packages/flutter_tools/lib/src/proxied_devices/devices.dart index 63d005eb43..3db86da98c 100644 --- a/packages/flutter_tools/lib/src/proxied_devices/devices.dart +++ b/packages/flutter_tools/lib/src/proxied_devices/devices.dart @@ -57,11 +57,14 @@ class ProxiedDevices extends DeviceDiscovery { List? _devices; @override - Future> get devices async => - _devices ?? await discoverDevices(); + Future> devices({DeviceDiscoveryFilter? filter}) async => + _devices ?? await discoverDevices(filter: filter); @override - Future> discoverDevices({Duration? timeout}) async { + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter + }) async { final List> discoveredDevices = _cast>(await connection.sendRequest('device.discoverDevices')).cast>(); final List devices = [ for (final Map device in discoveredDevices) diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index cb808a3919..08378e0627 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1491,7 +1491,7 @@ abstract class FlutterCommand extends Command { /// If no device can be found that meets specified criteria, /// then print an error message and return null. Future?> findAllTargetDevices({ - bool includeUnsupportedDevices = false, + bool includeDevicesUnsupportedByProject = false, }) async { if (!globals.doctor!.canLaunchAnything) { globals.printError(userMessages.flutterNoDevelopmentDevice); @@ -1499,14 +1499,14 @@ abstract class FlutterCommand extends Command { } final DeviceManager deviceManager = globals.deviceManager!; List devices = await deviceManager.findTargetDevices( - includeUnsupportedDevices ? null : FlutterProject.current(), + includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject, timeout: deviceDiscoveryTimeout, ); if (devices.isEmpty) { if (deviceManager.hasSpecifiedDeviceId) { globals.logger.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId!)); - final List allDevices = await deviceManager.getAllConnectedDevices(); + final List allDevices = await deviceManager.getAllDevices(); if (allDevices.isNotEmpty) { globals.logger.printStatus(''); globals.logger.printStatus('The following devices were found:'); @@ -1543,7 +1543,7 @@ abstract class FlutterCommand extends Command { } else { // Show an error message asking the user to specify `-d all` if they // want to run on multiple devices. - final List allDevices = await deviceManager.getAllConnectedDevices(); + final List allDevices = await deviceManager.getAllDevices(); globals.logger.printStatus(userMessages.flutterSpecifyDeviceWithAllOption); globals.logger.printStatus(''); await Device.printDevices(allDevices, globals.logger); @@ -1607,18 +1607,20 @@ abstract class FlutterCommand extends Command { /// If a device cannot be found that meets specified criteria, /// then print an error message and return null. /// - /// If [includeUnsupportedDevices] is true, the tool does not filter + /// If [includeDevicesUnsupportedByProject] is true, the tool does not filter /// the list by the current project support list. Future findTargetDevice({ - bool includeUnsupportedDevices = false, + bool includeDevicesUnsupportedByProject = false, }) async { - List? deviceList = await findAllTargetDevices(includeUnsupportedDevices: includeUnsupportedDevices); + List? deviceList = await findAllTargetDevices( + includeDevicesUnsupportedByProject: includeDevicesUnsupportedByProject, + ); if (deviceList == null) { return null; } if (deviceList.length > 1) { globals.printStatus(userMessages.flutterSpecifyDevice); - deviceList = await globals.deviceManager!.getAllConnectedDevices(); + deviceList = await globals.deviceManager!.getAllDevices(); globals.printStatus(''); await Device.printDevices(deviceList, globals.logger); return null; 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 4e716d37a0..1c43780dba 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart @@ -1240,6 +1240,9 @@ class FakeAndroidDevice extends Fake implements AndroidDevice { @override bool isSupported() => true; + @override + bool get isConnected => true; + @override bool get supportsHotRestart => true; @@ -1340,6 +1343,9 @@ class FakeIOSDevice extends Fake implements IOSDevice { @override bool isSupportedForProject(FlutterProject project) => true; + + @override + bool get isConnected => true; } class FakeMDnsClient extends Fake implements MDnsClient { 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 cc0e3d4d5d..85f40c9dd0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart @@ -50,7 +50,7 @@ void main() { testUsingContext("get devices' platform types", () async { final List platformTypes = Device.devicesPlatformTypes( - await globals.deviceManager!.getAllConnectedDevices(), + await globals.deviceManager!.getAllDevices(), ); expect(platformTypes, ['android', 'web']); }, overrides: { @@ -134,12 +134,14 @@ class _FakeDeviceManager extends DeviceManager { _FakeDeviceManager() : super(logger: testLogger); @override - Future> getAllConnectedDevices() => + Future> getAllDevices({DeviceDiscoveryFilter? filter}) => Future>.value(fakeDevices.map((FakeDeviceJsonData d) => d.dev).toList()); @override - Future> refreshAllConnectedDevices({Duration? timeout}) => - getAllConnectedDevices(); + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) => getAllDevices(filter: filter); @override Future> getDeviceDiagnostics() => Future>.value( @@ -154,11 +156,16 @@ class NoDevicesManager extends DeviceManager { NoDevicesManager() : super(logger: testLogger); @override - Future> getAllConnectedDevices() async => []; + Future> getAllDevices({ + DeviceDiscoveryFilter? filter, + }) async => []; @override - Future> refreshAllConnectedDevices({Duration? timeout}) => - getAllConnectedDevices(); + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) => + getAllDevices(); @override List get deviceDiscoverers => []; 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 7941bf0573..a5ea59e6f0 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/doctor_test.dart @@ -1192,7 +1192,9 @@ class FakeDeviceManager extends Fake implements DeviceManager { List devices = []; @override - Future> getAllConnectedDevices() async => devices; + Future> getAllDevices({ + 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 5ab23c4362..04cf404149 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart @@ -587,10 +587,16 @@ class FakeDeviceManager extends Fake implements DeviceManager { String? specifiedDeviceId; @override - Future> getDevices() async => devices; + Future> getDevices({ + DeviceDiscoveryFilter? filter, + }) async => devices; @override - Future> findTargetDevices(FlutterProject? flutterProject, {Duration? timeout, bool promptUserToChooseDevice = true}) async => devices; + Future> findTargetDevices({ + bool includeDevicesUnsupportedByProject = false, + Duration? timeout, + bool promptUserToChooseDevice = true, + }) async => devices; } /// A [FlutterDriverFactory] that creates a [NeverEndingDriverService]. 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 fed7e60214..6612e93c36 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 @@ -98,7 +98,7 @@ void main() { final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); - final List devices = await proxiedDevices.devices; + final List devices = await proxiedDevices.devices(); expect(devices, hasLength(1)); final Device device = devices[0]; final bool supportsRuntimeMode = await device.supportsRuntimeMode(BuildMode.release); @@ -121,7 +121,7 @@ void main() { final FakeDeviceLogReader fakeLogReader = FakeDeviceLogReader(); fakeDevice.logReader = fakeLogReader; - final List devices = await proxiedDevices.devices; + final List devices = await proxiedDevices.devices(); expect(devices, hasLength(1)); final Device device = devices[0]; final DeviceLogReader logReader = await device.getLogReader(); @@ -153,7 +153,7 @@ void main() { dummyApplicationBinary.writeAsStringSync('dummy content'); prebuiltApplicationPackage.applicationPackage = dummyApplicationBinary; - final List devices = await proxiedDevices.devices; + final List devices = await proxiedDevices.devices(); expect(devices, hasLength(1)); final Device device = devices[0]; @@ -200,7 +200,7 @@ void main() { final ProxiedDevices proxiedDevices = ProxiedDevices(clientDaemonConnection, logger: bufferLogger); - final List devices = await proxiedDevices.devices; + final List devices = await proxiedDevices.devices(); expect(devices, hasLength(1)); final Device device = devices[0]; 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 9a8a6feca5..9df39cd3b9 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -1054,6 +1054,9 @@ class FakeDevice extends Fake implements Device { @override bool get supportsFastStart => false; + @override + bool get isConnected => true; + bool supported = true; @override diff --git a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart index ed189deed1..7b65e6232a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/test_test.dart @@ -920,7 +920,9 @@ class _FakeDeviceManager extends DeviceManager { final List _connectedDevices; @override - Future> getAllConnectedDevices() async => _connectedDevices; + Future> getAllDevices({ + DeviceDiscoveryFilter? filter, + }) async => _connectedDevices; @override List get deviceDiscoverers => []; diff --git a/packages/flutter_tools/test/commands.shard/permeable/devices_test.dart b/packages/flutter_tools/test/commands.shard/permeable/devices_test.dart index c1e16e1c30..4eaf0b4ad5 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/devices_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/devices_test.dart @@ -88,7 +88,10 @@ class FakeDeviceManager extends Fake implements DeviceManager { String? specifiedDeviceId; @override - Future> refreshAllConnectedDevices({Duration? timeout}) async { + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async { return devices; } } diff --git a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart index 7a56f6c5aa..981ead6085 100644 --- a/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart +++ b/packages/flutter_tools/test/general.shard/custom_devices/custom_device_test.dart @@ -166,7 +166,7 @@ void main() { directory: dir, logger: BufferLogger.test() ) - ).devices, []); + ).devices(), []); }); testWithoutContext('CustomDevice: no devices listed if custom devices feature flag disabled', () async { @@ -184,7 +184,7 @@ void main() { directory: dir, logger: BufferLogger.test() ) - ).devices, []); + ).devices(), []); }); testWithoutContext('CustomDevices.devices', () async { @@ -208,7 +208,7 @@ void main() { directory: dir, logger: BufferLogger.test() ) - ).devices, + ).devices(), hasLength(1) ); }); diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index 6b4e1bcc7a..dcd6b2bc23 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -16,6 +16,7 @@ import 'package:flutter_tools/src/project.dart'; import 'package:test/fake.dart'; import '../src/common.dart'; +import '../src/context.dart'; import '../src/fake_devices.dart'; void main() { @@ -123,30 +124,47 @@ void main() { expect(logger.traceText, contains('Ignored error discovering Nexus')); }); - testWithoutContext('getAllConnectedDevices caches', () async { + testWithoutContext('getDeviceById two exact matches, matches on first', () async { final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); - final TestDeviceManager deviceManager = TestDeviceManager( - [device1], - logger: BufferLogger.test(), - ); - expect(await deviceManager.getAllConnectedDevices(), [device1]); + final FakeDevice device2 = FakeDevice('Nexus 5', '01abfc49119c410e'); + final List devices = [device1, device2]; + final BufferLogger logger = BufferLogger.test(); - final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); - deviceManager.resetDevices([device2]); - expect(await deviceManager.getAllConnectedDevices(), [device1]); + final DeviceManager deviceManager = TestDeviceManager( + devices, + logger: logger, + ); + + Future expectDevice(String id, List expected) async { + expect(await deviceManager.getDevicesById(id), expected); + } + await expectDevice('Nexus 5', [device1]); }); - testWithoutContext('refreshAllConnectedDevices does not cache', () async { + testWithoutContext('getAllDevices caches', () async { final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); final TestDeviceManager deviceManager = TestDeviceManager( [device1], logger: BufferLogger.test(), ); - expect(await deviceManager.refreshAllConnectedDevices(), [device1]); + expect(await deviceManager.getAllDevices(), [device1]); final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); deviceManager.resetDevices([device2]); - expect(await deviceManager.refreshAllConnectedDevices(), [device2]); + expect(await deviceManager.getAllDevices(), [device1]); + }); + + 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 FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); + deviceManager.resetDevices([device2]); + expect(await deviceManager.refreshAllDevices(), [device2]); }); }); @@ -179,8 +197,10 @@ void main() { ..targetPlatform = Future.value(TargetPlatform.web_javascript); final FakeDevice fuchsiaDevice = FakeDevice('fuchsiay', 'fuchsiay') ..targetPlatform = Future.value(TargetPlatform.fuchsia_x64); + final FakeDevice unconnectedDevice = FakeDevice('ephemeralTwo', 'ephemeralTwo', isConnected: false); + final FakeDevice wirelessDevice = FakeDevice('ephemeralTwo', 'ephemeralTwo', connectionInterface: DeviceConnectionInterface.wireless); - testWithoutContext('chooses ephemeral device', () async { + testUsingContext('chooses ephemeral device', () async { final List devices = [ ephemeralOne, nonEphemeralOne, @@ -193,12 +213,14 @@ void main() { devices, logger: BufferLogger.test(), ); - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered.single, ephemeralOne); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('returns all devices when multiple non ephemeral devices are found', () async { + testUsingContext('returns all devices when multiple non ephemeral devices are found', () async { final List devices = [ ephemeralOne, ephemeralTwo, @@ -211,7 +233,7 @@ void main() { logger: BufferLogger.test(), ); - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, [ ephemeralOne, @@ -219,9 +241,11 @@ void main() { nonEphemeralOne, nonEphemeralTwo, ]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Unsupported devices listed in all connected devices', () async { + testUsingContext('Unsupported devices listed in all devices', () async { final List devices = [ unsupported, unsupportedForProject, @@ -231,15 +255,17 @@ void main() { devices, logger: BufferLogger.test(), ); - final List filtered = await deviceManager.getAllConnectedDevices(); + final List filtered = await deviceManager.getAllDevices(); expect(filtered, [ unsupported, unsupportedForProject, ]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Removes a unsupported devices', () async { + testUsingContext('Removes unsupported devices', () async { final List devices = [ unsupported, unsupportedForProject, @@ -248,12 +274,14 @@ void main() { devices, logger: BufferLogger.test(), ); - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, []); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Retains devices unsupported by the project if FlutterProject is null', () async { + testUsingContext('Retains devices unsupported by the project if FlutterProject is null', () async { final List devices = [ unsupported, unsupportedForProject, @@ -263,12 +291,16 @@ void main() { devices, logger: BufferLogger.test(), ); - final List filtered = await deviceManager.findTargetDevices(null); + final List filtered = await deviceManager.findTargetDevices( + includeDevicesUnsupportedByProject: true, + ); expect(filtered, [unsupportedForProject]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Removes web and fuchsia from --all', () async { + testUsingContext('Removes web and fuchsia from --all', () async { final List devices = [ webDevice, fuchsiaDevice, @@ -279,12 +311,16 @@ void main() { ); deviceManager.specifiedDeviceId = 'all'; - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices( + includeDevicesUnsupportedByProject: true, + ); expect(filtered, []); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Removes devices unsupported by the project from --all', () async { + testUsingContext('Removes devices unsupported by the project from --all', () async { final List devices = [ nonEphemeralOne, nonEphemeralTwo, @@ -297,15 +333,17 @@ void main() { ); deviceManager.specifiedDeviceId = 'all'; - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, [ nonEphemeralOne, nonEphemeralTwo, ]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Returns device with the specified id', () async { + testUsingContext('Returns device with the specified id', () async { final List devices = [ nonEphemeralOne, ]; @@ -315,14 +353,16 @@ void main() { ); deviceManager.specifiedDeviceId = nonEphemeralOne.id; - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, [ nonEphemeralOne, ]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Returns multiple devices when multiple devices matches the specified id', () async { + testUsingContext('Returns multiple devices when multiple devices matches the specified id', () async { final List devices = [ nonEphemeralOne, nonEphemeralTwo, @@ -333,15 +373,17 @@ void main() { ); deviceManager.specifiedDeviceId = 'nonEphemeral'; // This prefix matches both devices - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, [ nonEphemeralOne, nonEphemeralTwo, ]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('Returns empty when device of specified id is not found', () async { + testUsingContext('Returns empty when device of specified id is not found', () async { final List devices = [ nonEphemeralOne, ]; @@ -351,12 +393,14 @@ void main() { ); deviceManager.specifiedDeviceId = nonEphemeralTwo.id; - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered, []); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('uses DeviceManager.isDeviceSupportedForProject instead of device.isSupportedForProject', () async { + testWithoutContext('uses DeviceDiscoverySupportFilter.isDeviceSupportedForProject instead of device.isSupportedForProject', () async { final List devices = [ unsupported, unsupportedForProject, @@ -365,16 +409,106 @@ void main() { devices, logger: BufferLogger.test(), ); - deviceManager.isAlwaysSupportedForProjectOverride = true; + final TestDeviceDiscoverySupportFilter supportFilter = + TestDeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject( + flutterProject: FakeFlutterProject(), + ); + supportFilter.isAlwaysSupportedForProjectOverride = true; + final DeviceDiscoveryFilter filter = DeviceDiscoveryFilter( + supportFilter: supportFilter, + ); - final List filtered = await deviceManager.findTargetDevices(FakeFlutterProject()); + final List filtered = await deviceManager.getDevices( + filter: filter, + ); expect(filtered, [ unsupportedForProject, ]); }); - testWithoutContext('does not refresh device cache without a timeout', () async { + testUsingContext('Unconnencted devices filtered out by default', () async { + final List devices = [ + unconnectedDevice, + ]; + final DeviceManager deviceManager = TestDeviceManager( + devices, + logger: BufferLogger.test(), + ); + + final List filtered = await deviceManager.getDevices(); + + expect(filtered, []); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), + }); + + testUsingContext('Return unconnected devices when filter allows', () async { + final List devices = [ + unconnectedDevice, + ]; + final DeviceManager deviceManager = TestDeviceManager( + devices, + logger: BufferLogger.test(), + ); + final DeviceDiscoveryFilter filter = DeviceDiscoveryFilter( + excludeDisconnected: false, + ); + + final List filtered = await deviceManager.getDevices( + filter: filter, + ); + + expect(filtered, [unconnectedDevice]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), + }); + + testUsingContext('Filter to only include wireless devices', () async { + final List devices = [ + ephemeralOne, + wirelessDevice, + ]; + final DeviceManager deviceManager = TestDeviceManager( + devices, + logger: BufferLogger.test(), + ); + final DeviceDiscoveryFilter filter = DeviceDiscoveryFilter( + deviceConnectionInterface: DeviceConnectionInterface.wireless, + ); + + final List filtered = await deviceManager.getDevices( + filter: filter, + ); + + expect(filtered, [wirelessDevice]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), + }); + + testUsingContext('Filter to only include attached devices', () async { + final List devices = [ + ephemeralOne, + wirelessDevice, + ]; + final DeviceManager deviceManager = TestDeviceManager( + devices, + logger: BufferLogger.test(), + ); + final DeviceDiscoveryFilter filter = DeviceDiscoveryFilter( + deviceConnectionInterface: DeviceConnectionInterface.attached, + ); + + final List filtered = await deviceManager.getDevices( + filter: filter, + ); + + expect(filtered, [ephemeralOne]); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), + }); + + testUsingContext('does not refresh device cache without a timeout', () async { final List devices = [ ephemeralOne, ]; @@ -389,16 +523,16 @@ void main() { logger: BufferLogger.test(), ); deviceManager.specifiedDeviceId = ephemeralOne.id; - final List filtered = await deviceManager.findTargetDevices( - FakeFlutterProject(), - ); + final List filtered = await deviceManager.findTargetDevices(); expect(filtered.single, ephemeralOne); expect(deviceDiscovery.devicesCalled, 1); expect(deviceDiscovery.discoverDevicesCalled, 0); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), }); - testWithoutContext('refreshes device cache with a timeout', () async { + testUsingContext('refreshes device cache with a timeout', () async { final List devices = [ ephemeralOne, ]; @@ -415,13 +549,161 @@ void main() { ); deviceManager.specifiedDeviceId = ephemeralOne.id; final List filtered = await deviceManager.findTargetDevices( - FakeFlutterProject(), timeout: timeout, ); expect(filtered.single, ephemeralOne); expect(deviceDiscovery.devicesCalled, 1); expect(deviceDiscovery.discoverDevicesCalled, 1); + }, overrides: { + FlutterProject: () => FakeFlutterProject(), + }); + }); + + + + group('Simultaneous device discovery', () { + testWithoutContext('Run getAllDevices and refreshAllDevices at same time with refreshAllDevices finishing last', () async { + FakeAsync().run((FakeAsync time) { + final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); + final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); + + const Duration timeToGetInitialDevices = Duration(seconds: 1); + const Duration timeToRefreshDevices = Duration(seconds: 5); + final List initialDevices = [device2]; + final List refreshDevices = [device1]; + + final TestDeviceManager deviceManager = TestDeviceManager( + [], + logger: BufferLogger.test(), + fakeDiscoverer: FakePollingDeviceDiscoveryWithTimeout( + >[ + initialDevices, + refreshDevices, + ], + timeout: timeToGetInitialDevices, + ), + ); + + // Expect that the cache is set by getOrSetCache process (1 second timeout) + // and then later updated by refreshCache process (5 second timeout). + // Ending with devices from the refreshCache process. + final Future> refreshCache = deviceManager.refreshAllDevices( + timeout: timeToRefreshDevices, + ); + final Future> getOrSetCache = deviceManager.getAllDevices(); + + // After 1 second, the getAllDevices should be done + time.elapse(const Duration(seconds: 1)); + expect(getOrSetCache, completion([device2])); + // double check values in cache are as expected + Future> getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device2])); + + // After 5 seconds, getOrSetCache should be done + time.elapse(const Duration(seconds: 5)); + expect(refreshCache, completion([device1])); + // double check values in cache are as expected + getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device1])); + + time.flushMicrotasks(); + }); + }); + + testWithoutContext('Run getAllDevices and refreshAllDevices at same time with refreshAllDevices finishing first', () async { + fakeAsync((FakeAsync async) { + final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); + final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); + + const Duration timeToGetInitialDevices = Duration(seconds: 5); + const Duration timeToRefreshDevices = Duration(seconds: 1); + final List initialDevices = [device2]; + final List refreshDevices = [device1]; + + final TestDeviceManager deviceManager = TestDeviceManager( + [], + logger: BufferLogger.test(), + fakeDiscoverer: FakePollingDeviceDiscoveryWithTimeout( + >[ + initialDevices, + refreshDevices, + ], + timeout: timeToGetInitialDevices, + ), + ); + + // Expect that the cache is set by refreshCache process (1 second timeout). + // Then later when getOrSetCache finishes (5 second timeout), it does not update the cache. + // Ending with devices from the refreshCache process. + final Future> refreshCache = deviceManager.refreshAllDevices( + timeout: timeToRefreshDevices, + ); + final Future> getOrSetCache = deviceManager.getAllDevices(); + + // After 1 second, the refreshCache should be done + async.elapse(const Duration(seconds: 1)); + expect(refreshCache, completion([device2])); + // double check values in cache are as expected + Future> getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device2])); + + // After 5 seconds, getOrSetCache should be done + async.elapse(const Duration(seconds: 5)); + expect(getOrSetCache, completion([device2])); + // double check values in cache are as expected + getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device2])); + + async.flushMicrotasks(); + }); + }); + + testWithoutContext('refreshAllDevices twice', () async { + fakeAsync((FakeAsync async) { + final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); + final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); + + const Duration timeToFirstRefresh = Duration(seconds: 1); + const Duration timeToSecondRefresh = Duration(seconds: 5); + final List firstRefreshDevices = [device2]; + final List secondRefreshDevices = [device1]; + + final TestDeviceManager deviceManager = TestDeviceManager( + [], + logger: BufferLogger.test(), + fakeDiscoverer: FakePollingDeviceDiscoveryWithTimeout( + >[ + firstRefreshDevices, + secondRefreshDevices, + ], + ), + ); + + // Expect that the cache is updated by each refresh in order of completion. + final Future> firstRefresh = deviceManager.refreshAllDevices( + timeout: timeToFirstRefresh, + ); + final Future> secondRefresh = deviceManager.refreshAllDevices( + timeout: timeToSecondRefresh, + ); + + // After 1 second, the firstRefresh should be done + async.elapse(const Duration(seconds: 1)); + expect(firstRefresh, completion([device2])); + // double check values in cache are as expected + Future> getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device2])); + + // After 5 seconds, secondRefresh should be done + async.elapse(const Duration(seconds: 5)); + expect(secondRefresh, completion([device1])); + // double check values in cache are as expected + getFromCache = deviceManager.getAllDevices(); + expect(getFromCache, completion([device1])); + + async.flushMicrotasks(); + }); }); }); @@ -744,7 +1026,8 @@ class TestDeviceManager extends DeviceManager { List? deviceDiscoveryOverrides, required super.logger, String? wellKnownId, - }) : _fakeDeviceDiscoverer = FakePollingDeviceDiscovery(), + FakePollingDeviceDiscovery? fakeDiscoverer, + }) : _fakeDeviceDiscoverer = fakeDiscoverer ?? FakePollingDeviceDiscovery(), _deviceDiscoverers = [], super() { if (wellKnownId != null) { @@ -764,16 +1047,6 @@ class TestDeviceManager extends DeviceManager { void resetDevices(List allDevices) { _fakeDeviceDiscoverer.setDevices(allDevices); } - - bool? isAlwaysSupportedForProjectOverride; - - @override - bool isDeviceSupportedForProject(Device device, FlutterProject? flutterProject) { - if (isAlwaysSupportedForProjectOverride != null) { - return isAlwaysSupportedForProjectOverride!; - } - return super.isDeviceSupportedForProject(device, flutterProject); - } } class MockDeviceDiscovery extends Fake implements DeviceDiscovery { @@ -786,13 +1059,16 @@ class MockDeviceDiscovery extends Fake implements DeviceDiscovery { List deviceValues = []; @override - Future> get devices async { + Future> devices({DeviceDiscoveryFilter? filter}) async { devicesCalled += 1; return deviceValues; } @override - Future> discoverDevices({Duration? timeout}) async { + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async { discoverDevicesCalled += 1; return deviceValues; } @@ -801,6 +1077,43 @@ class MockDeviceDiscovery extends Fake implements DeviceDiscovery { List get wellKnownIds => []; } +class TestDeviceDiscoverySupportFilter extends DeviceDiscoverySupportFilter { + TestDeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject({ + required super.flutterProject, + }) : super.excludeDevicesUnsupportedByFlutterOrProject(); + + + bool? isAlwaysSupportedForProjectOverride; + + @override + bool isDeviceSupportedForProject(Device device) { + if (isAlwaysSupportedForProjectOverride != null) { + return isAlwaysSupportedForProjectOverride!; + } + return super.isDeviceSupportedForProject(device); + } +} + +class FakePollingDeviceDiscoveryWithTimeout extends FakePollingDeviceDiscovery { + FakePollingDeviceDiscoveryWithTimeout( + this._devices, { + Duration? timeout, + }): defaultTimeout = timeout ?? const Duration(seconds: 2); + + final List> _devices; + int index = 0; + + Duration defaultTimeout; + @override + Future> pollingGetDevices({ Duration? timeout }) async { + timeout ??= defaultTimeout; + await Future.delayed(timeout); + final List results = _devices[index]; + index += 1; + return results; + } +} + class FakeFlutterProject extends Fake implements FlutterProject { } class LongPollingDeviceDiscovery extends PollingDeviceDiscovery { diff --git a/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart b/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart index 79bf8d2e9a..6abc4bf332 100644 --- a/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart +++ b/packages/flutter_tools/test/general.shard/linux/linux_device_test.dart @@ -67,7 +67,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), operatingSystemUtils: FakeOperatingSystemUtils(), - ).devices, []); + ).devices(), []); }); testWithoutContext('LinuxDevice: no devices listed if Linux feature flag disabled', () async { @@ -78,7 +78,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), operatingSystemUtils: FakeOperatingSystemUtils(), - ).devices, []); + ).devices(), []); }); testWithoutContext('LinuxDevice: devices', () async { @@ -89,7 +89,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), operatingSystemUtils: FakeOperatingSystemUtils(), - ).devices, hasLength(1)); + ).devices(), hasLength(1)); }); testWithoutContext('LinuxDevice has well known id "linux"', () async { diff --git a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart index 6d20cc870e..f2d77e5494 100644 --- a/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/macos_device_test.dart @@ -93,7 +93,7 @@ void main() { featureFlags: TestFeatureFlags(isMacOSEnabled: true), platform: linux, ), - ).devices, isEmpty); + ).devices(), isEmpty); }); testWithoutContext('No devices listed if platform is supported and feature is disabled', () async { @@ -109,7 +109,7 @@ void main() { ), ); - expect(await macOSDevices.devices, isEmpty); + expect(await macOSDevices.devices(), isEmpty); }); testWithoutContext('devices listed if platform is supported and feature is enabled', () async { @@ -125,7 +125,7 @@ void main() { ), ); - expect(await macOSDevices.devices, hasLength(1)); + expect(await macOSDevices.devices(), hasLength(1)); }); testWithoutContext('has a well known device id macos', () async { diff --git a/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart b/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart index 01493f5fb8..c960ccaff0 100644 --- a/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/macos_ipad_device_test.dart @@ -50,7 +50,7 @@ void main() { ); expect(discoverer.supportsPlatform, isTrue); - final List devices = await discoverer.devices; + final List devices = await discoverer.devices(); expect(devices, isEmpty); }); @@ -66,7 +66,7 @@ void main() { ); expect(discoverer.supportsPlatform, isTrue); - final List devices = await discoverer.devices; + final List devices = await discoverer.devices(); expect(devices, isEmpty); }); @@ -82,7 +82,7 @@ void main() { ); expect(discoverer.supportsPlatform, isTrue); - final List devices = await discoverer.devices; + final List devices = await discoverer.devices(); expect(devices, isEmpty); }); @@ -98,7 +98,7 @@ void main() { ); expect(discoverer.supportsPlatform, isTrue); - List devices = await discoverer.devices; + List devices = await discoverer.devices(); expect(devices, hasLength(1)); final Device device = devices.single; diff --git a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart index c284f76ef8..6cc826e7de 100644 --- a/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart +++ b/packages/flutter_tools/test/general.shard/tester/flutter_tester_test.dart @@ -47,7 +47,7 @@ void main() { testWithoutContext('no device', () async { final FlutterTesterDevices discoverer = setUpFlutterTesterDevices(); - final List devices = await discoverer.devices; + final List devices = await discoverer.devices(); expect(devices, isEmpty); }); @@ -55,7 +55,7 @@ void main() { FlutterTesterDevices.showFlutterTesterDevice = true; final FlutterTesterDevices discoverer = setUpFlutterTesterDevices(); - final List devices = await discoverer.devices; + final List devices = await discoverer.devices(); expect(devices, hasLength(1)); final Device device = devices.single; diff --git a/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart index 99387e676f..1f38de3ac5 100644 --- a/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart @@ -48,7 +48,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), - ).devices, []); + ).devices(), []); }); testWithoutContext('WindowsDevices lists a devices if the workflow is supported', () async { @@ -61,7 +61,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), - ).devices, hasLength(1)); + ).devices(), hasLength(1)); }); testWithoutContext('isSupportedForProject is true with editable host app', () async { diff --git a/packages/flutter_tools/test/src/context.dart b/packages/flutter_tools/test/src/context.dart index 8a2dd5d02c..d2d94fa6d8 100644 --- a/packages/flutter_tools/test/src/context.dart +++ b/packages/flutter_tools/test/src/context.dart @@ -207,21 +207,31 @@ class FakeDeviceManager implements DeviceManager { } @override - Future> getAllConnectedDevices() async => devices; + Future> getAllDevices({ + DeviceDiscoveryFilter? filter, + }) async => devices; @override - Future> refreshAllConnectedDevices({ Duration? timeout }) async => devices; + Future> refreshAllDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) async => devices; @override - Future> getDevicesById(String deviceId) async { + Future> getDevicesById( + String deviceId, { + DeviceDiscoveryFilter? filter, + }) async { return devices.where((Device device) => device.id == deviceId).toList(); } @override - Future> getDevices() { + Future> getDevices({ + DeviceDiscoveryFilter? filter, + }) { return hasSpecifiedDeviceId - ? getDevicesById(specifiedDeviceId!) - : getAllConnectedDevices(); + ? getDevicesById(specifiedDeviceId!, filter: filter) + : getAllDevices(filter: filter); } void addDevice(Device device) => devices.add(device); @@ -236,16 +246,27 @@ class FakeDeviceManager implements DeviceManager { List get deviceDiscoverers => []; @override - bool isDeviceSupportedForProject(Device device, FlutterProject? flutterProject) { - return device.isSupportedForProject(flutterProject!); + Future> findTargetDevices({ + bool includeDevicesUnsupportedByProject = false, + Duration? timeout, + bool promptUserToChooseDevice = true, + }) async { + return devices; } @override - Future> findTargetDevices(FlutterProject? flutterProject, { Duration? timeout, bool promptUserToChooseDevice = true }) async { - return devices; + DeviceDiscoverySupportFilter deviceSupportFilter({ + bool includeDevicesUnsupportedByProject = false, + FlutterProject? flutterProject, + }) { + return TestDeviceDiscoverySupportFilter(); } } +class TestDeviceDiscoverySupportFilter extends Fake implements DeviceDiscoverySupportFilter { + TestDeviceDiscoverySupportFilter(); +} + class FakeAndroidLicenseValidator extends Fake implements AndroidLicenseValidator { @override Future get licensesAccepted async => LicensesAccepted.all; diff --git a/packages/flutter_tools/test/src/fake_devices.dart b/packages/flutter_tools/test/src/fake_devices.dart index d19cf4a339..9463c153e3 100644 --- a/packages/flutter_tools/test/src/fake_devices.dart +++ b/packages/flutter_tools/test/src/fake_devices.dart @@ -62,6 +62,8 @@ class FakeDevice extends Device { bool ephemeral = true, bool isSupported = true, bool isSupportedForProject = true, + this.isConnected = true, + this.connectionInterface = DeviceConnectionInterface.attached, PlatformType type = PlatformType.web, LaunchResult? launchResult, }) : _isSupported = isSupported, @@ -118,6 +120,12 @@ class FakeDevice extends Device { @override bool isSupported() => _isSupported; + @override + bool isConnected; + + @override + DeviceConnectionInterface connectionInterface; + @override Future isLocalEmulator = Future.value(true); @@ -174,7 +182,10 @@ class FakePollingDeviceDiscovery extends PollingDeviceDiscovery { bool discoverDevicesCalled = false; @override - Future> discoverDevices({Duration? timeout}) { + Future> discoverDevices({ + Duration? timeout, + DeviceDiscoveryFilter? filter, + }) { discoverDevicesCalled = true; return super.discoverDevices(timeout: timeout); }