[flutter_tool] Improve Fuchsia 'run' tests (#33263)

diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart
index a110ac8..b3a9b77 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_dev_finder.dart
@@ -4,6 +4,7 @@
 
 import '../base/common.dart';
 import '../base/process.dart';
+import '../globals.dart';
 import 'fuchsia_sdk.dart';
 
 // Usage: dev_finder <flags> <subcommand> <subcommand args>
@@ -31,7 +32,11 @@
       '-full'
     ];
     final RunResult result = await runAsync(command);
-    return (result.exitCode == 0) ? result.stdout.split('\n') : null;
+    if (result.exitCode != 0) {
+      printError('dev_finder failed: ${result.stderr}');
+      return null;
+    }
+    return result.stdout.split('\n');
   }
 
   /// Returns the host address by which the device [deviceName] should use for
@@ -51,6 +56,10 @@
       deviceName
     ];
     final RunResult result = await runAsync(command);
-    return (result.exitCode == 0) ? result.stdout.trim() : null;
+    if (result.exitCode != 0) {
+      printError('dev_finder failed: ${result.stderr}');
+      return null;
+    }
+    return result.stdout.trim();
   }
 }
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index b48aac8..3a82092 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -44,9 +44,6 @@
   FuchsiaTilesCtl get tilesCtl => _tilesCtl ??= FuchsiaTilesCtl();
 }
 
-final FuchsiaAmberCtl _amberCtl = fuchsiaDeviceTools.amberCtl;
-final FuchsiaTilesCtl _tilesCtl = fuchsiaDeviceTools.tilesCtl;
-
 final String _ipv4Loopback = InternetAddress.loopbackIPv4.address;
 final String _ipv6Loopback = InternetAddress.loopbackIPv6.address;
 
@@ -233,6 +230,10 @@
     await stopApp(package);
     // Find out who the device thinks we are.
     final String host = await fuchsiaSdk.fuchsiaDevFinder.resolve(name);
+    if (host == null) {
+      printError('Failed to resolve host for Fuchsia device');
+      return LaunchResult.failed();
+    }
     final int port = await os.findFreePort();
     if (port == 0) {
       printError('Failed to find a free port');
@@ -265,14 +266,14 @@
       }
 
       // Teach amber about the package server.
-      if (!await _amberCtl.addSrc(this, fuchsiaPackageServer)) {
+      if (!await fuchsiaDeviceTools.amberCtl.addSrc(this, fuchsiaPackageServer)) {
         printError('Failed to teach amber about the package server');
         return LaunchResult.failed();
       }
       serverRegistered = true;
 
       // Tell amber to prefetch the app.
-      if (!await _amberCtl.getUp(this, appName)) {
+      if (!await fuchsiaDeviceTools.amberCtl.getUp(this, appName)) {
         printError('Failed to get amber to prefetch the package');
         return LaunchResult.failed();
       }
@@ -286,14 +287,14 @@
       // Instruct tiles_ctl to start the app.
       final String fuchsiaUrl =
           'fuchsia-pkg://fuchsia.com/$appName#meta/$appName.cmx';
-      if (!await _tilesCtl.add(this, fuchsiaUrl, <String>[])) {
+      if (!await fuchsiaDeviceTools.tilesCtl.add(this, fuchsiaUrl, <String>[])) {
         printError('Failed to add the app to tiles');
         return LaunchResult.failed();
       }
     } finally {
       // Try to un-teach amber about the package server if needed.
       if (serverRegistered) {
-        await _amberCtl.rmSrc(this, fuchsiaPackageServer);
+        await fuchsiaDeviceTools.amberCtl.rmSrc(this, fuchsiaPackageServer);
       }
       // Shutdown the package server and delete the package repo;
       fuchsiaPackageServer.stop();
@@ -308,7 +309,7 @@
 
     // In a debug or profile build, try to find the observatory uri.
     final FuchsiaIsolateDiscoveryProtocol discovery =
-        FuchsiaIsolateDiscoveryProtocol(this, appName);
+        getIsolateDiscoveryProtocol(appName);
     try {
       final Uri observatoryUri = await discovery.uri;
       return LaunchResult.succeeded(observatoryUri: observatoryUri);
@@ -321,7 +322,7 @@
   Future<bool> stopApp(covariant FuchsiaApp app) async {
     final int appKey = await FuchsiaTilesCtl.findAppKey(this, app.id);
     if (appKey != -1) {
-      if (!await _tilesCtl.remove(this, appKey)) {
+      if (!await fuchsiaDeviceTools.tilesCtl.remove(this, appKey)) {
         printError('tiles_ctl remove on ${app.id} failed.');
         return false;
       }
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
index c2af066..0cda7a7 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_sdk.dart
@@ -50,6 +50,9 @@
       return null;
     }
     final List<String> devices = await fuchsiaDevFinder.list();
+    if (devices == null) {
+      return null;
+    }
     return devices.isNotEmpty ? devices[0] : null;
   }
 
diff --git a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
index 735df55..7e10b66 100644
--- a/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
+++ b/packages/flutter_tools/test/fuchsia/fuchsa_device_test.dart
@@ -407,32 +407,49 @@
   group('fuchsia app start and stop: ', () {
     MemoryFileSystem memoryFileSystem;
     MockOperatingSystemUtils osUtils;
-    MockFuchsiaDeviceTools fuchsiaDeviceTools;
+    FakeFuchsiaDeviceTools fuchsiaDeviceTools;
     MockFuchsiaSdk fuchsiaSdk;
     setUp(() {
       memoryFileSystem = MemoryFileSystem();
       osUtils = MockOperatingSystemUtils();
-      fuchsiaDeviceTools = MockFuchsiaDeviceTools();
+      fuchsiaDeviceTools = FakeFuchsiaDeviceTools();
       fuchsiaSdk = MockFuchsiaSdk();
 
       when(osUtils.findFreePort()).thenAnswer((_) => Future<int>.value(12345));
     });
 
-    testUsingContext('start prebuilt app in release mode', () async {
+    Future<LaunchResult> setupAndStartApp({
+      @required bool prebuilt,
+      @required BuildMode mode,
+    }) async {
       const String appName = 'app_name';
-      final FuchsiaDevice device = FuchsiaDevice('123');
+      final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123');
       fs.directory('fuchsia').createSync(recursive: true);
       final File pubspecFile = fs.file('pubspec.yaml')..createSync();
       pubspecFile.writeAsStringSync('name: $appName');
-      final File far = fs.file('app_name-0.far')..createSync();
 
-      final FuchsiaApp app = FuchsiaApp.fromPrebuiltApp(far);
+      FuchsiaApp app;
+      if (prebuilt) {
+        final File far = fs.file('app_name-0.far')..createSync();
+        app = FuchsiaApp.fromPrebuiltApp(far);
+      } else {
+        fs.file(fs.path.join('fuchsia', 'meta', '$appName.cmx'))
+          .createSync(recursive: true);
+        fs.file('.packages').createSync();
+        fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
+        app = BuildableFuchsiaApp(project: FlutterProject.current().fuchsia);
+      }
+
       final DebuggingOptions debuggingOptions =
-          DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null));
-      final LaunchResult launchResult = await device.startApp(app,
-          prebuiltApplication: true,
+          DebuggingOptions.disabled(BuildInfo(mode, null));
+      return await device.startApp(app,
+          prebuiltApplication: prebuilt,
           debuggingOptions: debuggingOptions);
+    }
 
+    testUsingContext('start prebuilt in release mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
       expect(launchResult.started, isTrue);
       expect(launchResult.hasObservatory, isFalse);
     }, overrides: <Type, Generator>{
@@ -442,9 +459,9 @@
       OperatingSystemUtils: () => osUtils,
     });
 
-    testUsingContext('start and stop prebuilt app in release mode', () async {
+    testUsingContext('start and stop prebuilt in release mode', () async {
       const String appName = 'app_name';
-      final FuchsiaDevice device = FuchsiaDevice('123');
+      final FuchsiaDevice device = FuchsiaDeviceWithFakeDiscovery('123');
       fs.directory('fuchsia').createSync(recursive: true);
       final File pubspecFile = fs.file('pubspec.yaml')..createSync();
       pubspecFile.writeAsStringSync('name: $appName');
@@ -456,7 +473,6 @@
       final LaunchResult launchResult = await device.startApp(app,
           prebuiltApplication: true,
           debuggingOptions: debuggingOptions);
-
       expect(launchResult.started, isTrue);
       expect(launchResult.hasObservatory, isFalse);
       expect(await device.stopApp(app), isTrue);
@@ -466,6 +482,91 @@
       FuchsiaSdk: () => fuchsiaSdk,
       OperatingSystemUtils: () => osUtils,
     });
+
+    testUsingContext('start prebuilt in debug mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.debug);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start buildable in release mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: false, mode: BuildMode.release);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('start buildable in debug mode', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: false, mode: BuildMode.debug);
+      expect(launchResult.started, isTrue);
+      expect(launchResult.hasObservatory, isTrue);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when dev_finder fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => MockFuchsiaSdk(devFinder: FailingDevFinder()),
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when pm fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => fuchsiaDeviceTools,
+      FuchsiaSdk: () => MockFuchsiaSdk(pm: FailingPM()),
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when amber fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => FakeFuchsiaDeviceTools(amber: FailingAmberCtl()),
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
+    testUsingContext('fail with correct LaunchResult when tiles fails', () async {
+      final LaunchResult launchResult =
+          await setupAndStartApp(prebuilt: true, mode: BuildMode.release);
+      expect(launchResult.started, isFalse);
+      expect(launchResult.hasObservatory, isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => memoryFileSystem,
+      FuchsiaDeviceTools: () => FakeFuchsiaDeviceTools(tiles: FailingTilesCtl()),
+      FuchsiaSdk: () => fuchsiaSdk,
+      OperatingSystemUtils: () => osUtils,
+    });
+
   });
 }
 
@@ -559,7 +660,24 @@
   final String name;
 }
 
-class MockFuchsiaAmberCtl extends Mock implements FuchsiaAmberCtl {
+class FuchsiaDeviceWithFakeDiscovery extends FuchsiaDevice {
+  FuchsiaDeviceWithFakeDiscovery(String id, {String name}) : super(id, name: name);
+
+  @override
+  FuchsiaIsolateDiscoveryProtocol getIsolateDiscoveryProtocol(
+        String isolateName) =>
+    FakeFuchsiaIsolateDiscoveryProtocol();
+}
+
+class FakeFuchsiaIsolateDiscoveryProtocol implements FuchsiaIsolateDiscoveryProtocol {
+  @override
+  FutureOr<Uri> get uri => Uri.parse('http://[::1]:37');
+
+  @override
+  void dispose() {}
+}
+
+class FakeFuchsiaAmberCtl implements FuchsiaAmberCtl {
   @override
   Future<bool> addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
     return true;
@@ -576,7 +694,24 @@
   }
 }
 
-class MockFuchsiaTilesCtl extends Mock implements FuchsiaTilesCtl {
+class FailingAmberCtl implements FuchsiaAmberCtl {
+  @override
+  Future<bool> addSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+
+  @override
+  Future<bool> rmSrc(FuchsiaDevice device, FuchsiaPackageServer server) async {
+    return false;
+  }
+
+  @override
+  Future<bool> getUp(FuchsiaDevice device, String packageName) async {
+    return false;
+  }
+}
+
+class FakeFuchsiaTilesCtl implements FuchsiaTilesCtl {
   final Map<int, String> _runningApps = <int, String>{};
   bool _started = false;
   int _nextAppId = 1;
@@ -624,15 +759,48 @@
   }
 }
 
-class MockFuchsiaDeviceTools extends Mock implements FuchsiaDeviceTools {
+class FailingTilesCtl implements FuchsiaTilesCtl {
   @override
-  final FuchsiaAmberCtl amberCtl = MockFuchsiaAmberCtl();
+  Future<bool> start(FuchsiaDevice device) async {
+    return false;
+  }
 
   @override
-  final FuchsiaTilesCtl tilesCtl = MockFuchsiaTilesCtl();
+  Future<Map<int, String>> list(FuchsiaDevice device) async {
+    return null;
+  }
+
+  @override
+  Future<bool> add(FuchsiaDevice device, String url, List<String> args) async {
+    return false;
+  }
+
+  @override
+  Future<bool> remove(FuchsiaDevice device, int key) async {
+    return false;
+  }
+
+  @override
+  Future<bool> quit(FuchsiaDevice device) async {
+    return false;
+  }
 }
 
-class MockFuchsiaPM extends Mock implements FuchsiaPM {
+class FakeFuchsiaDeviceTools implements FuchsiaDeviceTools {
+  FakeFuchsiaDeviceTools({
+    FuchsiaAmberCtl amber,
+    FuchsiaTilesCtl tiles,
+  }) : amberCtl = amber ?? FakeFuchsiaAmberCtl(),
+       tilesCtl = tiles ?? FakeFuchsiaTilesCtl();
+
+  @override
+  final FuchsiaAmberCtl amberCtl;
+
+  @override
+  final FuchsiaTilesCtl tilesCtl;
+}
+
+class FakeFuchsiaPM implements FuchsiaPM {
   String _appName;
 
   @override
@@ -710,7 +878,46 @@
   }
 }
 
-class MockFuchsiaKernelCompiler extends Mock implements FuchsiaKernelCompiler {
+class FailingPM implements FuchsiaPM {
+  @override
+  Future<bool> init(String buildPath, String appName) async {
+    return false;
+  }
+
+  @override
+  Future<bool> genkey(String buildPath, String outKeyPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> build(
+      String buildPath, String keyPath, String manifestPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> archive(
+      String buildPath, String keyPath, String manifestPath) async {
+    return false;
+  }
+
+  @override
+  Future<bool> newrepo(String repoPath) async {
+    return false;
+  }
+
+  @override
+  Future<Process> serve(String repoPath, String host, int port) async {
+    return _createMockProcess(exitCode: 6);
+  }
+
+  @override
+  Future<bool> publish(String repoPath, String packagePath) async {
+    return false;
+  }
+}
+
+class FakeFuchsiaKernelCompiler implements FuchsiaKernelCompiler {
   @override
   Future<void> build({
     @required FuchsiaProject fuchsiaProject,
@@ -724,7 +931,18 @@
   }
 }
 
-class MockFuchsiaDevFinder extends Mock implements FuchsiaDevFinder {
+class FailingKernelCompiler implements FuchsiaKernelCompiler {
+  @override
+  Future<void> build({
+    @required FuchsiaProject fuchsiaProject,
+    @required String target, // E.g., lib/main.dart
+    BuildInfo buildInfo = BuildInfo.debug,
+  }) async {
+    throwToolExit('Build process failed');
+  }
+}
+
+class FakeFuchsiaDevFinder implements FuchsiaDevFinder {
   @override
   Future<List<String>> list() async {
     return <String>['192.168.42.172 scare-cable-skip-joy'];
@@ -736,14 +954,33 @@
   }
 }
 
+class FailingDevFinder implements FuchsiaDevFinder {
+  @override
+  Future<List<String>> list() async {
+    return null;
+  }
+
+  @override
+  Future<String> resolve(String deviceName) async {
+    return null;
+  }
+}
+
 class MockFuchsiaSdk extends Mock implements FuchsiaSdk {
-  @override
-  final FuchsiaPM fuchsiaPM = MockFuchsiaPM();
+  MockFuchsiaSdk({
+    FuchsiaPM pm,
+    FuchsiaKernelCompiler compiler,
+    FuchsiaDevFinder devFinder,
+  }) : fuchsiaPM = pm ?? FakeFuchsiaPM(),
+       fuchsiaKernelCompiler = compiler ?? FakeFuchsiaKernelCompiler(),
+       fuchsiaDevFinder = devFinder ?? FakeFuchsiaDevFinder();
 
   @override
-  final FuchsiaKernelCompiler fuchsiaKernelCompiler =
-      MockFuchsiaKernelCompiler();
+  final FuchsiaPM fuchsiaPM;
 
   @override
-  final FuchsiaDevFinder fuchsiaDevFinder = MockFuchsiaDevFinder();
+  final FuchsiaKernelCompiler fuchsiaKernelCompiler;
+
+  @override
+  final FuchsiaDevFinder fuchsiaDevFinder;
 }