[flutter_tools] support multiple fuchsia devices (#55780)

Fixes #55765

We are currently only returning the first device from dev-finder, instead we need to look at the whole list.
diff --git a/packages/flutter_tools/bin/fuchsia_attach.dart b/packages/flutter_tools/bin/fuchsia_attach.dart
index 6de061a..59e00fe 100644
--- a/packages/flutter_tools/bin/fuchsia_attach.dart
+++ b/packages/flutter_tools/bin/fuchsia_attach.dart
@@ -17,6 +17,7 @@
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/fuchsia/fuchsia_device.dart';
 import 'package:flutter_tools/src/fuchsia/fuchsia_sdk.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
 import 'package:flutter_tools/src/project.dart';
 import 'package:flutter_tools/src/runner/flutter_command.dart';
@@ -126,7 +127,12 @@
 class _FuchsiaDeviceManager extends DeviceManager {
   @override
   List<DeviceDiscovery> get deviceDiscoverers => List<DeviceDiscovery>.unmodifiable(<DeviceDiscovery>[
-    FuchsiaDevices(),
+    FuchsiaDevices(
+      logger: globals.logger,
+      platform: globals.platform,
+      fuchsiaWorkflow: fuchsiaWorkflow,
+      fuchsiaSdk: fuchsiaSdk,
+    ),
   ]);
 
   @override
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index e7df4b3..61d46ff 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -19,6 +19,8 @@
 import 'build_info.dart';
 import 'features.dart';
 import 'fuchsia/fuchsia_device.dart';
+import 'fuchsia/fuchsia_sdk.dart';
+import 'fuchsia/fuchsia_workflow.dart';
 import 'globals.dart' as globals;
 import 'ios/devices.dart';
 import 'ios/simulators.dart';
@@ -82,7 +84,12 @@
       iosWorkflow: globals.iosWorkflow,
     ),
     IOSSimulators(iosSimulatorUtils: globals.iosSimulatorUtils),
-    FuchsiaDevices(),
+    FuchsiaDevices(
+      fuchsiaSdk: fuchsiaSdk,
+      logger: globals.logger,
+      fuchsiaWorkflow: fuchsiaWorkflow,
+      platform: globals.platform,
+    ),
     FlutterTesterDevices(),
     MacOSDevices(),
     LinuxDevices(
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index 0ec719a..9cee2f6 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -5,6 +5,7 @@
 import 'dart:async';
 
 import 'package:meta/meta.dart';
+import 'package:platform/platform.dart';
 import 'package:vm_service/vm_service.dart' as vm_service;
 
 import '../application_package.dart';
@@ -136,56 +137,75 @@
   }
 }
 
+/// Device discovery for Fuchsia devices.
 class FuchsiaDevices extends PollingDeviceDiscovery {
-  FuchsiaDevices() : super('Fuchsia devices');
+  FuchsiaDevices({
+    @required Platform platform,
+    @required FuchsiaWorkflow fuchsiaWorkflow,
+    @required FuchsiaSdk fuchsiaSdk,
+    @required Logger logger,
+  }) : _platform = platform,
+       _fuchsiaWorkflow = fuchsiaWorkflow,
+       _fuchsiaSdk = fuchsiaSdk,
+       _logger = logger,
+       super('Fuchsia devices');
+
+  final Platform _platform;
+  final FuchsiaWorkflow _fuchsiaWorkflow;
+  final FuchsiaSdk _fuchsiaSdk;
+  final Logger _logger;
 
   @override
-  bool get supportsPlatform => isFuchsiaSupportedPlatform();
+  bool get supportsPlatform => isFuchsiaSupportedPlatform(_platform);
 
   @override
-  bool get canListAnything => fuchsiaWorkflow.canListDevices;
+  bool get canListAnything => _fuchsiaWorkflow.canListDevices;
 
   @override
   Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
-    if (!fuchsiaWorkflow.canListDevices) {
+    if (!_fuchsiaWorkflow.canListDevices) {
       return <Device>[];
     }
-    final String text = await fuchsiaSdk.listDevices(timeout: timeout);
+    final List<String> text = (await _fuchsiaSdk.listDevices(timeout: timeout))
+      ?.split('\n');
     if (text == null || text.isEmpty) {
       return <Device>[];
     }
-    final List<FuchsiaDevice> devices = await parseListDevices(text);
+    final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
+    for (final String line in text) {
+      final FuchsiaDevice device = await _parseDevice(line);
+      if (device == null) {
+        continue;
+      }
+      devices.add(device);
+    }
     return devices;
   }
 
   @override
   Future<List<String>> getDiagnostics() async => const <String>[];
-}
 
-@visibleForTesting
-Future<List<FuchsiaDevice>> parseListDevices(String text) async {
-  final List<FuchsiaDevice> devices = <FuchsiaDevice>[];
-  for (final String rawLine in text.trim().split('\n')) {
-    final String line = rawLine.trim();
+  Future<FuchsiaDevice> _parseDevice(String text) async {
+    final String line = text.trim();
     // ['ip', 'device name']
     final List<String> words = line.split(' ');
     if (words.length < 2) {
-      continue;
+      return null;
     }
     final String name = words[1];
-    final String resolvedHost = await fuchsiaSdk.fuchsiaDevFinder.resolve(
+    final String resolvedHost = await _fuchsiaSdk.fuchsiaDevFinder.resolve(
       name,
       local: false,
     );
     if (resolvedHost == null) {
-      globals.printError('Failed to resolve host for Fuchsia device `$name`');
-      continue;
+      _logger.printError('Failed to resolve host for Fuchsia device `$name`');
+      return null;
     }
-    devices.add(FuchsiaDevice(resolvedHost, name: name));
+    return FuchsiaDevice(resolvedHost, name: name);
   }
-  return devices;
 }
 
+
 class FuchsiaDevice extends Device {
   FuchsiaDevice(String id, {this.name}) : super(
       id,
@@ -451,7 +471,7 @@
   }
 
   @override
-  bool get supportsScreenshot => isFuchsiaSupportedPlatform();
+  bool get supportsScreenshot => isFuchsiaSupportedPlatform(globals.platform);
 
   @override
   Future<void> takeScreenshot(File outputFile) async {
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
index f86bbd0..16bc964 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
@@ -4,6 +4,8 @@
 
 import 'dart:async';
 
+import 'package:platform/platform.dart';
+
 import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
@@ -18,8 +20,8 @@
 FuchsiaSdk get fuchsiaSdk => context.get<FuchsiaSdk>();
 
 /// Returns [true] if the current platform supports Fuchsia targets.
-bool isFuchsiaSupportedPlatform() {
-  return globals.platform.isLinux || globals.platform.isMacOS;
+bool isFuchsiaSupportedPlatform(Platform platform) {
+  return platform.isLinux || platform.isMacOS;
 }
 
 /// The Fuchsia SDK shell commands.
@@ -45,6 +47,8 @@
   FuchsiaKernelCompiler get fuchsiaKernelCompiler =>
       _fuchsiaKernelCompiler ??= FuchsiaKernelCompiler();
 
+  /// Returns any attached devices is a newline-denominated String.
+  ///
   /// Example output:
   ///    $ device-finder list -full
   ///    > 192.168.42.56 paper-pulp-bush-angel
@@ -57,7 +61,7 @@
     if (devices == null) {
       return null;
     }
-    return devices.isNotEmpty ? devices[0] : null;
+    return devices.isNotEmpty ? devices.join('\n') : null;
   }
 
   /// Returns the fuchsia system logs for an attached device where
@@ -116,7 +120,7 @@
   /// FUCHSIA_SSH_CONFIG) to find the ssh configuration needed to talk to
   /// a device.
   factory FuchsiaArtifacts.find() {
-    if (!isFuchsiaSupportedPlatform()) {
+    if (!isFuchsiaSupportedPlatform(globals.platform)) {
       // Don't try to find the artifacts on platforms that are not supported.
       return FuchsiaArtifacts();
     }
diff --git a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
index dd91b10..06ab541 100644
--- a/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/fuchsia/fuchsia_device_test.dart
@@ -6,6 +6,8 @@
 import 'dart:convert';
 
 import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
 import 'package:vm_service/vm_service.dart' as vm_service;
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
@@ -66,32 +68,89 @@
       when(sshConfig.absolute).thenReturn(sshConfig);
     });
 
-    testUsingContext('stores the requested id and name', () {
+    testWithoutContext('stores the requested id and name', () {
       const String deviceId = 'e80::0000:a00a:f00f:2002/3';
       const String name = 'halfbaked';
       final FuchsiaDevice device = FuchsiaDevice(deviceId, name: name);
+
       expect(device.id, deviceId);
       expect(device.name, name);
     });
 
-    testUsingContext('parse device-finder output', () async {
-      const String example = '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel';
-      final List<FuchsiaDevice> names = await parseListDevices(example);
+    testWithoutContext('lists nothing when workflow cannot list devices', () async {
+      final MockFuchsiaWorkflow fuchsiaWorkflow = MockFuchsiaWorkflow();
+      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
+        platform: FakePlatform(operatingSystem: 'linux'),
+        fuchsiaSdk: null,
+        fuchsiaWorkflow: fuchsiaWorkflow,
+        logger: BufferLogger.test(),
+      );
+      when(fuchsiaWorkflow.canListDevices).thenReturn(false);
 
-      expect(names.length, 1);
-      expect(names.first.name, 'paper-pulp-bush-angel');
-      expect(names.first.id, '192.168.42.10');
-    }, overrides: <Type, Generator>{
-      FuchsiaSdk: () => MockFuchsiaSdk(),
+      expect(fuchsiaDevices.canListAnything, false);
+      expect(await fuchsiaDevices.pollingGetDevices(), isEmpty);
     });
 
-    testUsingContext('parse junk device-finder output', () async {
-      const String example = 'junk';
-      final List<FuchsiaDevice> names = await parseListDevices(example);
+    testWithoutContext('can parse device-finder output for single device', () async {
+      final MockFuchsiaWorkflow fuchsiaWorkflow = MockFuchsiaWorkflow();
+      final MockFuchsiaSdk fuchsiaSdk = MockFuchsiaSdk();
+      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
+        platform: FakePlatform(operatingSystem: 'linux'),
+        fuchsiaSdk: fuchsiaSdk,
+        fuchsiaWorkflow: fuchsiaWorkflow,
+        logger: BufferLogger.test(),
+      );
+      when(fuchsiaWorkflow.canListDevices).thenReturn(true);
+      when(fuchsiaSdk.listDevices()).thenAnswer((Invocation invocation) async {
+        return '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel';
+      });
 
-      expect(names.length, 0);
-    }, overrides: <Type, Generator>{
-      FuchsiaSdk: () => MockFuchsiaSdk(),
+      final Device device = (await fuchsiaDevices.pollingGetDevices()).single;
+
+      expect(device.name, 'paper-pulp-bush-angel');
+      expect(device.id, '192.168.42.10');
+    });
+
+    testWithoutContext('can parse device-finder output for multiple devices', () async {
+      final MockFuchsiaWorkflow fuchsiaWorkflow = MockFuchsiaWorkflow();
+      final MockFuchsiaSdk fuchsiaSdk = MockFuchsiaSdk();
+      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
+        platform: FakePlatform(operatingSystem: 'linux'),
+        fuchsiaSdk: fuchsiaSdk,
+        fuchsiaWorkflow: fuchsiaWorkflow,
+        logger: BufferLogger.test(),
+      );
+      when(fuchsiaWorkflow.canListDevices).thenReturn(true);
+      when(fuchsiaSdk.listDevices()).thenAnswer((Invocation invocation) async {
+        return '2001:0db8:85a3:0000:0000:8a2e:0370:7334 paper-pulp-bush-angel\n'
+          '2001:0db8:85a3:0000:0000:8a2e:0370:7335 foo-bar-fiz-buzz';
+      });
+
+      final List<Device> devices = await fuchsiaDevices.pollingGetDevices();
+
+      expect(devices.first.name, 'paper-pulp-bush-angel');
+      expect(devices.first.id, '192.168.42.10');
+      expect(devices.last.name, 'foo-bar-fiz-buzz');
+      expect(devices.last.id, '192.168.42.10');
+    });
+
+    testWithoutContext('can parse junk output from the dev-finder', () async {
+      final MockFuchsiaWorkflow fuchsiaWorkflow = MockFuchsiaWorkflow();
+      final MockFuchsiaSdk fuchsiaSdk = MockFuchsiaSdk();
+      final FuchsiaDevices fuchsiaDevices = FuchsiaDevices(
+        platform: FakePlatform(operatingSystem: 'linux'),
+        fuchsiaSdk: fuchsiaSdk,
+        fuchsiaWorkflow: fuchsiaWorkflow,
+        logger: BufferLogger.test(),
+      );
+      when(fuchsiaWorkflow.canListDevices).thenReturn(true);
+      when(fuchsiaSdk.listDevices()).thenAnswer((Invocation invocation) async {
+        return 'junk';
+      });
+
+      final List<Device> devices = await fuchsiaDevices.pollingGetDevices();
+
+      expect(devices, isEmpty);
     });
 
     testUsingContext('disposing device disposes the portForwarder', () async {
@@ -118,6 +177,7 @@
 
     test('is ephemeral', () {
       final FuchsiaDevice device = FuchsiaDevice('123');
+
       expect(device.ephemeral, true);
     });
 
@@ -125,6 +185,7 @@
       final FuchsiaDevice device = FuchsiaDevice('123');
       globals.fs.directory('fuchsia').createSync(recursive: true);
       globals.fs.file('pubspec.yaml').createSync();
+
       expect(device.isSupportedForProject(FlutterProject.current()), true);
     }, overrides: <Type, Generator>{
       FileSystem: () => memoryFileSystem,
@@ -134,6 +195,7 @@
     testUsingContext('not supported for project', () async {
       final FuchsiaDevice device = FuchsiaDevice('123');
       globals.fs.file('pubspec.yaml').createSync();
+
       expect(device.isSupportedForProject(FlutterProject.current()), false);
     }, overrides: <Type, Generator>{
       FileSystem: () => memoryFileSystem,
@@ -142,6 +204,7 @@
 
     testUsingContext('targetPlatform does not throw when sshConfig is missing', () async {
       final FuchsiaDevice device = FuchsiaDevice('123');
+
       expect(await device.targetPlatform, TargetPlatform.fuchsia_arm64);
     }, overrides: <Type, Generator>{
       FuchsiaArtifacts: () => FuchsiaArtifacts(sshConfig: null),
@@ -1475,3 +1538,5 @@
   @override
   final FuchsiaDevFinder fuchsiaDevFinder;
 }
+
+class MockFuchsiaWorkflow extends Mock implements FuchsiaWorkflow {}