diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 1130db61f30..1723582c3c7 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -104,23 +104,51 @@ abstract class DeviceManager { bool get hasSpecifiedAllDevices => _specifiedDeviceId == 'all'; Future> getDevicesById(String deviceId) async { - final List devices = await getAllConnectedDevices(); - deviceId = deviceId.toLowerCase(); + final String lowerDeviceId = deviceId.toLowerCase(); bool exactlyMatchesDeviceId(Device device) => - device.id.toLowerCase() == deviceId || - device.name.toLowerCase() == deviceId; + device.id.toLowerCase() == lowerDeviceId || + device.name.toLowerCase() == lowerDeviceId; bool startsWithDeviceId(Device device) => - device.id.toLowerCase().startsWith(deviceId) || - device.name.toLowerCase().startsWith(deviceId); + device.id.toLowerCase().startsWith(lowerDeviceId) || + device.name.toLowerCase().startsWith(lowerDeviceId); - final Device exactMatch = devices.firstWhere( - exactlyMatchesDeviceId, orElse: () => null); - if (exactMatch != null) { - return [exactMatch]; + // Some discoverers have hard-coded device IDs and return quickly, and others + // shell out to other processes and can take longer. + // Process discoverers as they can return results, so if an exact match is + // found quickly, we don't wait for all the discoverers to complete. + final List prefixMatches = []; + final Completer exactMatchCompleter = Completer(); + final List>> futureDevices = >>[ + for (final DeviceDiscovery discoverer in _platformDiscoverers) + discoverer + .devices + .then((List devices) { + for (final Device device in devices) { + if (exactlyMatchesDeviceId(device)) { + exactMatchCompleter.complete(device); + return null; + } + if (startsWithDeviceId(device)) { + prefixMatches.add(device); + } + } + return null; + }, onError: (dynamic error, StackTrace stackTrace) { + // Return matches from other discoverers even if one fails. + globals.printTrace('Ignored error discovering $deviceId: $error'); + }) + ]; + + // Wait for an exact match, or for all discoverers to return results. + await Future.any(>[ + exactMatchCompleter.future, + Future.wait>(futureDevices), + ]); + + if (exactMatchCompleter.isCompleted) { + return [await exactMatchCompleter.future]; } - - // Match on a id or name starting with [deviceId]. - return devices.where(startsWithDeviceId).toList(); + return prefixMatches; } /// Returns the list of connected devices, filtered by any user-specified device id. diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart index 0fcb5422610..1c1b3afd15c 100644 --- a/packages/flutter_tools/test/general.shard/device_test.dart +++ b/packages/flutter_tools/test/general.shard/device_test.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/build_info.dart'; @@ -22,9 +23,11 @@ import '../src/mocks.dart'; void main() { MockCache cache; + BufferLogger logger; setUp(() { cache = MockCache(); + logger = BufferLogger.test(); when(cache.dyLdLibEntry).thenReturn(const MapEntry('foo', 'bar')); }); @@ -41,25 +44,67 @@ void main() { Cache: () => cache, }); - testUsingContext('getDeviceById', () async { + testUsingContext('getDeviceById exact matcher', () async { final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); final FakeDevice device3 = FakeDevice('iPod touch', '82564b38861a9a5'); final List devices = [device1, device2, device3]; - final DeviceManager deviceManager = TestDeviceManager(devices); + + // Include different device discoveries: + // 1. One that never completes to prove the first exact match is + // returned quickly. + // 2. One that throws, to prove matches can return when some succeed + // and others fail. + // 3. A device discoverer that succeeds. + final DeviceManager deviceManager = TestDeviceManager( + devices, + testLongPollingDeviceDiscovery: true, + testThrowingDeviceDiscovery: true, + ); Future expectDevice(String id, List expected) async { expect(await deviceManager.getDevicesById(id), expected); } await expectDevice('01abfc49119c410e', [device2]); + expect(logger.traceText, contains('Ignored error discovering 01abfc49119c410e')); await expectDevice('Nexus 5X', [device2]); + expect(logger.traceText, contains('Ignored error discovering Nexus 5X')); await expectDevice('0553790d0a4e726f', [device1]); - await expectDevice('Nexus 5', [device1]); - await expectDevice('0553790', [device1]); - await expectDevice('Nexus', [device1, device2]); + expect(logger.traceText, contains('Ignored error discovering 0553790d0a4e726f')); }, overrides: { Artifacts: () => Artifacts.test(), Cache: () => cache, + Logger: () => logger, + }); + + testUsingContext('getDeviceById prefix matcher', () async { + final FakeDevice device1 = FakeDevice('Nexus 5', '0553790d0a4e726f'); + final FakeDevice device2 = FakeDevice('Nexus 5X', '01abfc49119c410e'); + final FakeDevice device3 = FakeDevice('iPod touch', '82564b38861a9a5'); + final List devices = [device1, device2, device3]; + + // Include different device discoveries: + // 1. One that throws, to prove matches can return when some succeed + // and others fail. + // 2. A device discoverer that succeeds. + final DeviceManager deviceManager = TestDeviceManager( + devices, + testThrowingDeviceDiscovery: true + ); + + Future expectDevice(String id, List expected) async { + expect(await deviceManager.getDevicesById(id), expected); + } + await expectDevice('Nexus 5', [device1]); + expect(logger.traceText, contains('Ignored error discovering Nexus 5')); + await expectDevice('0553790', [device1]); + expect(logger.traceText, contains('Ignored error discovering 0553790')); + await expectDevice('Nexus', [device1, device2]); + expect(logger.traceText, contains('Ignored error discovering Nexus')); + }, overrides: { + Artifacts: () => Artifacts.test(), + Cache: () => cache, + Logger: () => logger, }); testUsingContext('getAllConnectedDevices caches', () async { @@ -374,16 +419,27 @@ void main() { } class TestDeviceManager extends DeviceManager { - TestDeviceManager(List allDevices) { - _deviceDiscoverer = FakePollingDeviceDiscovery(); + TestDeviceManager(List allDevices, { + bool testLongPollingDeviceDiscovery = false, + bool testThrowingDeviceDiscovery = false, + }) { + _fakeDeviceDiscoverer = FakePollingDeviceDiscovery(); + _deviceDiscoverers = [ + if (testLongPollingDeviceDiscovery) + LongPollingDeviceDiscovery(), + if (testThrowingDeviceDiscovery) + ThrowingPollingDeviceDiscovery(), + _fakeDeviceDiscoverer, + ]; resetDevices(allDevices); } @override - List get deviceDiscoverers => [_deviceDiscoverer]; - FakePollingDeviceDiscovery _deviceDiscoverer; + List get deviceDiscoverers => _deviceDiscoverers; + List _deviceDiscoverers; + FakePollingDeviceDiscovery _fakeDeviceDiscoverer; void resetDevices(List allDevices) { - _deviceDiscoverer.setDevices(allDevices); + _fakeDeviceDiscoverer.setDevices(allDevices); } bool isAlwaysSupportedOverride; diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index 66c5aa94bb2..e00ac548d12 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart @@ -526,6 +526,48 @@ class FakePollingDeviceDiscovery extends PollingDeviceDiscovery { Stream get onRemoved => _onRemovedController.stream; } +class LongPollingDeviceDiscovery extends PollingDeviceDiscovery { + LongPollingDeviceDiscovery() : super('forever'); + + final Completer> _completer = Completer>(); + + @override + Future> pollingGetDevices({ Duration timeout }) async { + return _completer.future; + } + + @override + Future stopPolling() async { + _completer.complete(); + } + + @override + Future dispose() async { + _completer.complete(); + } + + @override + bool get supportsPlatform => true; + + @override + bool get canListAnything => true; +} + +class ThrowingPollingDeviceDiscovery extends PollingDeviceDiscovery { + ThrowingPollingDeviceDiscovery() : super('throw'); + + @override + Future> pollingGetDevices({ Duration timeout }) async { + throw const ProcessException('fake-discovery', []); + } + + @override + bool get supportsPlatform => true; + + @override + bool get canListAnything => true; +} + class MockIosProject extends Mock implements IosProject { static const String bundleId = 'com.example.test'; static const String appBundleName = 'My Super Awesome App.app';