[flutter_tools] refactor iOS tests for Device.startApp into new file (#52854)

diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 6c4613d..88015c1 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -383,7 +383,7 @@
   @override
   DevicePortForwarder get portForwarder => _portForwarder ??= IOSDevicePortForwarder(
     processManager: globals.processManager,
-    logger: globals.logger,
+    logger: _logger,
     dyLdLibEntry: globals.cache.dyLdLibEntry,
     id: id,
     iproxyPath: _iproxyPath,
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 4f40715..9d27189 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/attach_test.dart
@@ -30,6 +30,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fakes.dart';
 import '../../src/mocks.dart';
 
 
@@ -54,7 +55,7 @@
       const int devicePort = 499;
       const int hostPort = 42;
 
-      MockDeviceLogReader mockLogReader;
+      FakeDeviceLogReader mockLogReader;
       MockPortForwarder portForwarder;
       MockAndroidDevice device;
       MockProcessManager mockProcessManager;
@@ -63,7 +64,7 @@
 
       setUp(() {
         mockProcessManager = MockProcessManager();
-        mockLogReader = MockDeviceLogReader();
+        mockLogReader = FakeDeviceLogReader();
         portForwarder = MockPortForwarder();
         device = MockAndroidDevice();
         vmServiceDoneCompleter = Completer<void>();
@@ -374,7 +375,7 @@
     testUsingContext('selects specified target', () async {
       const int devicePort = 499;
       const int hostPort = 42;
-      final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
+      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
       final MockPortForwarder portForwarder = MockPortForwarder();
       final MockAndroidDevice device = MockAndroidDevice();
       final MockHotRunner mockHotRunner = MockHotRunner();
@@ -434,7 +435,7 @@
     testUsingContext('fallbacks to protocol observatory if MDNS failed on iOS', () async {
       const int devicePort = 499;
       const int hostPort = 42;
-      final MockDeviceLogReader mockLogReader = MockDeviceLogReader();
+      final FakeDeviceLogReader mockLogReader = FakeDeviceLogReader();
       final MockPortForwarder portForwarder = MockPortForwarder();
       final MockIOSDevice device = MockIOSDevice();
       final MockHotRunner mockHotRunner = MockHotRunner();
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 2e04041..d694b5f 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
@@ -21,6 +21,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fakes.dart';
 import '../../src/mocks.dart';
 
 void main() {
@@ -351,7 +352,7 @@
         final Device mockDevice = MockDevice();
         testDeviceManager.addDevice(mockDevice);
 
-        final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader();
+        final FakeDeviceLogReader mockDeviceLogReader = FakeDeviceLogReader();
         when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader);
         final MockLaunchResult mockLaunchResult = MockLaunchResult();
         when(mockLaunchResult.started).thenReturn(true);
@@ -481,7 +482,7 @@
         final Device mockDevice = MockDevice();
         testDeviceManager.addDevice(mockDevice);
 
-        final MockDeviceLogReader mockDeviceLogReader = MockDeviceLogReader();
+        final FakeDeviceLogReader mockDeviceLogReader = FakeDeviceLogReader();
         when(mockDevice.getLogReader()).thenReturn(mockDeviceLogReader);
         final MockLaunchResult mockLaunchResult = MockLaunchResult();
         when(mockLaunchResult.started).thenReturn(true);
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 aab1f3b..ded8172 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
@@ -32,6 +32,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fakes.dart';
 import '../../src/mocks.dart';
 import '../../src/testbed.dart';
 
@@ -279,7 +280,7 @@
         applyMocksToCommand(command);
         final MockDevice mockDevice = MockDevice(TargetPlatform.ios);
         when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future<bool>.value(false));
-        when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(MockDeviceLogReader());
+        when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(FakeDeviceLogReader());
         when(mockDevice.supportsFastStart).thenReturn(true);
         when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future<String>.value('iOS 13'));
         // App fails to start because we're only interested in usage
@@ -633,7 +634,7 @@
 
   @override
   DeviceLogReader getLogReader({ ApplicationPackage app }) {
-    return MockDeviceLogReader();
+    return FakeDeviceLogReader();
   }
 
   @override
diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
index cf8f386..e454554 100644
--- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -36,6 +36,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/fakes.dart';
 import '../../src/mocks.dart';
 
 void main() {
@@ -395,12 +396,10 @@
       MockFileSystem mockFileSystem;
       MockPlatform mockPlatform;
       MockProcessManager mockProcessManager;
-      MockDeviceLogReader mockLogReader;
-      MockMDnsObservatoryDiscovery mockMDnsObservatoryDiscovery;
+      FakeDeviceLogReader mockLogReader;
       MockPortForwarder mockPortForwarder;
       MockIMobileDevice mockIMobileDevice;
       MockIOSDeploy mockIosDeploy;
-      MockUsage mockUsage;
 
       Directory tempDir;
       Directory projectDir;
@@ -428,13 +427,11 @@
         mockFileSystem = MockFileSystem();
         mockPlatform = MockPlatform();
         when(mockPlatform.isMacOS).thenReturn(true);
-        mockMDnsObservatoryDiscovery = MockMDnsObservatoryDiscovery();
         mockProcessManager = MockProcessManager();
-        mockLogReader = MockDeviceLogReader();
+        mockLogReader = FakeDeviceLogReader();
         mockPortForwarder = MockPortForwarder();
         mockIMobileDevice = MockIMobileDevice();
         mockIosDeploy = MockIOSDeploy();
-        mockUsage = MockUsage();
 
         tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
         projectDir = tempDir.childDirectory('flutter_project');
@@ -493,367 +490,6 @@
         Cache.enableLocking();
       });
 
-      testWithoutContext('disposing device disposes the portForwarder', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          platform: macPlatform,
-          iosDeploy: iosDeploy,
-          logger: logger,
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        device.portForwarder = mockPortForwarder;
-        device.setLogReader(mockApp, mockLogReader);
-        await device.dispose();
-        verify(mockPortForwarder.dispose()).called(1);
-      });
-
-      testUsingContext('succeeds in debug mode via mDNS', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          logger: logger,
-          platform: macPlatform,
-          iosDeploy: mockIosDeploy,
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: <String>[],
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        when(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        device.portForwarder = mockPortForwarder;
-        device.setLogReader(mockApp, mockLogReader);
-        final Uri uri = Uri(
-          scheme: 'http',
-          host: '127.0.0.1',
-          port: 1234,
-          path: 'observatory',
-        );
-        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
-          .thenAnswer((Invocation invocation) => Future<Uri>.value(uri));
-
-        final LaunchResult launchResult = await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
-          platformArgs: <String, dynamic>{},
-        );
-        verify(mockUsage.sendEvent('ios-handshake', 'mdns-success')).called(1);
-        expect(launchResult.started, isTrue);
-        expect(launchResult.hasObservatory, isTrue);
-        expect(await device.stopApp(mockApp), isFalse);
-      }, overrides: <Type, Generator>{
-        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
-        Usage: () => mockUsage,
-      });
-
-      testUsingContext('succeeds in debug mode when mDNS fails by falling back to manual protocol discovery', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          logger: logger,
-          platform: macPlatform,
-          iosDeploy: mockIosDeploy,
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        when(
-          mockIosDeploy.installApp(deviceId: device.id, bundlePath: anyNamed('bundlePath'), launchArguments: <String>[])
-        ).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        when(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        device.portForwarder = mockPortForwarder;
-        device.setLogReader(mockApp, mockLogReader);
-        // Now that the reader is used, start writing messages to it.
-        Timer.run(() {
-          mockLogReader.addLine('Foo');
-          mockLogReader.addLine('Observatory listening on http://127.0.0.1:$devicePort');
-        });
-        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
-          .thenAnswer((Invocation invocation) => Future<Uri>.value(null));
-
-        final LaunchResult launchResult = await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
-          platformArgs: <String, dynamic>{},
-        );
-        expect(launchResult.started, isTrue);
-        expect(launchResult.hasObservatory, isTrue);
-        verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
-        verify(mockUsage.sendEvent('ios-handshake', 'fallback-success')).called(1);
-        expect(await device.stopApp(mockApp), isFalse);
-      }, overrides: <Type, Generator>{
-        FileSystem: () => mockFileSystem,
-        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
-        ProcessManager: () => mockProcessManager,
-        Usage: () => mockUsage,
-      });
-
-      testUsingContext('fails in debug mode when mDNS fails and when Observatory URI is malformed', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          logger: logger,
-          platform: macPlatform,
-          iosDeploy: mockIosDeploy,
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: <String>[],
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        when(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        device.portForwarder = mockPortForwarder;
-        device.setLogReader(mockApp, mockLogReader);
-
-        // Now that the reader is used, start writing messages to it.
-        Timer.run(() {
-          mockLogReader.addLine('Foo');
-          mockLogReader.addLine('Observatory listening on http:/:/127.0.0.1:$devicePort');
-        });
-        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
-          .thenAnswer((Invocation invocation) => Future<Uri>.value(null));
-
-        final LaunchResult launchResult = await device.startApp(mockApp,
-            prebuiltApplication: true,
-            debuggingOptions: DebuggingOptions.enabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
-            platformArgs: <String, dynamic>{},
-        );
-        expect(launchResult.started, isFalse);
-        expect(launchResult.hasObservatory, isFalse);
-        verify(mockUsage.sendEvent(
-          'ios-handshake',
-          'failure-other',
-          label: anyNamed('label'),
-          value: anyNamed('value'),
-        )).called(1);
-        verify(mockUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
-        verify(mockUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1);
-      }, overrides: <Type, Generator>{
-        FileSystem: () => mockFileSystem,
-        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
-        ProcessManager: () => mockProcessManager,
-        Usage: () => mockUsage,
-      });
-
-      testUsingContext('succeeds in release mode', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          name: 'iPhone 1',
-          fileSystem: mockFileSystem,
-          sdkVersion: '13.3',
-          cpuArchitecture: DarwinArch.arm64,
-          logger: logger,
-          platform: mockPlatform,
-          artifacts: mockArtifacts,
-          iosDeploy: mockIosDeploy,
-        );
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: <String>[],
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        when(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        final LaunchResult launchResult = await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
-          platformArgs: <String, dynamic>{},
-        );
-        verify(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: <String>[],
-        ));
-        verify(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        ));
-        expect(launchResult.started, isTrue);
-        expect(launchResult.hasObservatory, isFalse);
-        expect(await device.stopApp(mockApp), isFalse);
-      });
-
-      testUsingContext('trace whitelist flags', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          name: 'iPhone 1',
-          fileSystem: mockFileSystem,
-          sdkVersion: '13.3',
-          cpuArchitecture: DarwinArch.arm64,
-          logger: logger,
-          platform: mockPlatform,
-          artifacts: mockArtifacts,
-          iosDeploy: mockIosDeploy,
-        );
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: <String>[],
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        when(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.disabled(
-            const BuildInfo(BuildMode.release, null, treeShakeIcons: false),
-            traceWhitelist: 'foo'),
-          platformArgs: <String, dynamic>{},
-        );
-        final VerificationResult toVerify = verify(mockIosDeploy.runApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: captureAnyNamed('launchArguments'),
-        ));
-        expect(toVerify.captured[0], contains('--trace-whitelist="foo"'));
-        await device.stopApp(mockApp);
-      });
-
-      testUsingContext('succeeds with --cache-sksl', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          logger: logger,
-          platform: macPlatform,
-          iosDeploy: mockIosDeploy,
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        device.setLogReader(mockApp, mockLogReader);
-        final Uri uri = Uri(
-          scheme: 'http',
-          host: '127.0.0.1',
-          port: 1234,
-          path: 'observatory',
-        );
-        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
-            .thenAnswer((Invocation invocation) => Future<Uri>.value(uri));
-
-        List<String> args;
-        when(mockIosDeploy.runApp(
-          deviceId: anyNamed('deviceId'),
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation inv) {
-          args = inv.namedArguments[const Symbol('launchArguments')] as List<String>;
-          return Future<int>.value(0);
-        });
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-
-        final LaunchResult launchResult = await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.enabled(
-              const BuildInfo(BuildMode.debug, null, treeShakeIcons: false),
-              cacheSkSL: true,
-          ),
-          platformArgs: <String, dynamic>{},
-        );
-        expect(launchResult.started, isTrue);
-        expect(args, contains('--cache-sksl'));
-        expect(await device.stopApp(mockApp), isFalse);
-      }, overrides: <Type, Generator>{
-        Artifacts: () => mockArtifacts,
-        Cache: () => mockCache,
-        FileSystem: () => mockFileSystem,
-        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
-        Platform: () => macPlatform,
-        ProcessManager: () => mockProcessManager,
-        Usage: () => mockUsage,
-        IOSDeploy: () => mockIosDeploy,
-      });
-
-      testUsingContext('succeeds with --device-vmservice-port', () async {
-        final IOSDevice device = IOSDevice(
-          '123',
-          name: 'iPhone 1',
-          sdkVersion: '13.3',
-          artifacts: mockArtifacts,
-          fileSystem: mockFileSystem,
-          logger: logger,
-          platform: macPlatform,
-          iosDeploy: mockIosDeploy,
-          cpuArchitecture: DarwinArch.arm64,
-        );
-        device.setLogReader(mockApp, mockLogReader);
-        final Uri uri = Uri(
-          scheme: 'http',
-          host: '127.0.0.1',
-          port: 1234,
-          path: 'observatory',
-        );
-        when(mockMDnsObservatoryDiscovery.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
-            .thenAnswer((Invocation invocation) => Future<Uri>.value(uri));
-
-        List<String> args;
-        when(mockIosDeploy.runApp(
-          deviceId: anyNamed('deviceId'),
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation inv) {
-          args = inv.namedArguments[const Symbol('launchArguments')] as List<String>;
-          return Future<int>.value(0);
-        });
-
-        when(mockIosDeploy.installApp(
-          deviceId: device.id,
-          bundlePath: anyNamed('bundlePath'),
-          launchArguments: anyNamed('launchArguments'),
-        )).thenAnswer((Invocation invocation) => Future<int>.value(0));
-        final LaunchResult launchResult = await device.startApp(mockApp,
-          prebuiltApplication: true,
-          debuggingOptions: DebuggingOptions.enabled(
-            const BuildInfo(BuildMode.debug, null, treeShakeIcons: false),
-            deviceVmServicePort: 8181,
-          ),
-          platformArgs: <String, dynamic>{},
-        );
-        expect(launchResult.started, isTrue);
-        expect(args, contains('--observatory-port=8181'));
-        expect(await device.stopApp(mockApp), isFalse);
-      }, overrides: <Type, Generator>{
-        Cache: () => mockCache,
-        MDnsObservatoryDiscovery: () => mockMDnsObservatoryDiscovery,
-        ProcessManager: () => mockProcessManager,
-        Usage: () => mockUsage,
-      });
-
       void testNonPrebuilt(
         String name, {
         @required bool showBuildSettingsFlakes,
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
new file mode 100644
index 0000000..826e0a9
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -0,0 +1,402 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/ios/devices.dart';
+import 'package:flutter_tools/src/ios/ios_deploy.dart';
+import 'package:flutter_tools/src/mdns_discovery.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/fakes.dart';
+
+const FakeCommand kDeployCommand = FakeCommand(
+  command: <String>[
+    'ios-deploy',
+    '--id',
+    '123',
+    '--bundle',
+    '/',
+    '--no-wifi',
+  ],
+  environment: <String, String>{
+    'PATH': '/usr/bin:null',
+    'DYLD_LIBRARY_PATH': '/path/to/libraries',
+  }
+);
+
+// The command used to actually launch the app with args in release/profile.
+const FakeCommand kLaunchReleaseCommand = FakeCommand(
+  command: <String>[
+    'ios-deploy',
+    '--id',
+    '123',
+    '--bundle',
+    '/',
+    '--no-wifi',
+    '--justlaunch',
+    // These args are the default on DebuggingOptions.
+    '--args',
+    '--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700',
+  ],
+  environment: <String, String>{
+    'PATH': '/usr/bin:null',
+    'DYLD_LIBRARY_PATH': '/path/to/libraries',
+  }
+);
+
+// The command used to actually launch the app with args in debug.
+const FakeCommand kLaunchDebugCommand = FakeCommand(command: <String>[
+  'ios-deploy',
+  '--id',
+  '123',
+  '--bundle',
+  '/',
+  '--no-wifi',
+  '--justlaunch',
+  '--args',
+  '--enable-dart-profiling --enable-service-port-fallback --disable-service-auth-codes --observatory-port=60700 --enable-checked-mode --verify-entry-points'
+], environment: <String, String>{
+  'PATH': '/usr/bin:null',
+  'DYLD_LIBRARY_PATH': '/path/to/libraries',
+});
+
+void main() {
+  // TODO(jonahwilliams): This test doesn't really belong here but
+  // I don't have a better place for it for now.
+  testWithoutContext('disposing device disposes the portForwarder and logReader', () async {
+    final IOSDevice device = setUpIOSDevice();
+    final DevicePortForwarder devicePortForwarder = MockDevicePortForwarder();
+    final DeviceLogReader deviceLogReader = MockDeviceLogReader();
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+    );
+
+    device.portForwarder = devicePortForwarder;
+    device.setLogReader(iosApp, deviceLogReader);
+    await device.dispose();
+
+    verify(deviceLogReader.dispose()).called(1);
+    verify(devicePortForwarder.dispose()).called(1);
+  });
+
+  // Still uses context for analytics and mDNS.
+  testUsingContext('IOSDevice.startApp succeeds in debug mode via mDNS discovery', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      kDeployCommand,
+      kLaunchDebugCommand,
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      bundleDir: fileSystem.currentDirectory,
+    );
+    final Uri uri = Uri(
+      scheme: 'http',
+      host: '127.0.0.1',
+      port: 1234,
+      path: 'observatory',
+    );
+
+    device.portForwarder = const NoOpDevicePortForwarder();
+    device.setLogReader(iosApp, FakeDeviceLogReader());
+
+    when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
+      .thenAnswer((Invocation invocation) async => uri);
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      platformArgs: <String, dynamic>{},
+    );
+
+    verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-success')).called(1);
+    expect(launchResult.started, true);
+    expect(launchResult.hasObservatory, true);
+    expect(await device.stopApp(iosApp), false);
+  }, overrides: <Type, Generator>{
+    MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
+    Usage: () => MockUsage(),
+  });
+
+  // Still uses context for analytics and mDNS.
+  testUsingContext('IOSDevice.startApp succeeds in debug mode when mDNS fails', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      kDeployCommand,
+      kLaunchDebugCommand,
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      bundleDir: fileSystem.currentDirectory,
+    );
+    final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+    device.portForwarder = const NoOpDevicePortForwarder();
+    device.setLogReader(iosApp, deviceLogReader);
+
+    // Now that the reader is used, start writing messages to it.
+    Timer.run(() {
+      deviceLogReader.addLine('Foo');
+      deviceLogReader.addLine('Observatory listening on http://127.0.0.1:456');
+    });
+    when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
+      .thenAnswer((Invocation invocation) async => null);
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      platformArgs: <String, dynamic>{},
+    );
+
+    expect(launchResult.started, true);
+    expect(launchResult.hasObservatory, true);
+    verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
+    verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-success')).called(1);
+    expect(await device.stopApp(iosApp), false);
+  }, overrides: <Type, Generator>{
+    Usage: () => MockUsage(),
+    MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
+  });
+
+  // Still uses context for analytics and mDNS.
+  testUsingContext('IOSDevice.startApp fails in debug mode when mDNS fails and '
+    'when Observatory URI is malformed', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      kDeployCommand,
+      kLaunchDebugCommand,
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      bundleDir: fileSystem.currentDirectory,
+    );
+    final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+    device.portForwarder = const NoOpDevicePortForwarder();
+    device.setLogReader(iosApp, deviceLogReader);
+
+    // Now that the reader is used, start writing messages to it.
+    Timer.run(() {
+      deviceLogReader.addLine('Foo');
+      deviceLogReader.addLine('Observatory listening on http:/:/127.0.0.1:456');
+    });
+    when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
+      .thenAnswer((Invocation invocation) async => null);
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      platformArgs: <String, dynamic>{},
+    );
+
+    expect(launchResult.started, false);
+    expect(launchResult.hasObservatory, false);
+    verify(globals.flutterUsage.sendEvent(
+      'ios-handshake',
+      'failure-other',
+      label: anyNamed('label'),
+      value: anyNamed('value'),
+    )).called(1);
+    verify(globals.flutterUsage.sendEvent('ios-handshake', 'mdns-failure')).called(1);
+    verify(globals.flutterUsage.sendEvent('ios-handshake', 'fallback-failure')).called(1);
+    }, overrides: <Type, Generator>{
+      MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
+      Usage: () => MockUsage(),
+    });
+
+  // Still uses context for TimeoutConfiguration and usage
+  testUsingContext('IOSDevice.startApp succeeds in release mode', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      kDeployCommand,
+      kLaunchReleaseCommand,
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      bundleDir: fileSystem.currentDirectory,
+    );
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+      platformArgs: <String, dynamic>{},
+    );
+
+    expect(launchResult.started, true);
+    expect(launchResult.hasObservatory, false);
+    expect(await device.stopApp(iosApp), false);
+    expect(processManager.hasRemainingExpectations, false);
+  }, overrides: <Type, Generator>{
+    Usage: () => MockUsage(),
+  });
+
+  // Still uses context for analytics and mDNS.
+  testUsingContext('IOSDevice.startApp forwards all supported debugging options', () async {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    final FakeProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
+      kDeployCommand,
+      FakeCommand(
+        command: <String>[
+          'ios-deploy',
+          '--id',
+          '123',
+          '--bundle',
+          '/',
+          '--no-wifi',
+          '--justlaunch',
+          // The arguments below are determined by what is passed into
+          // the debugging options argument to startApp.
+          '--args',
+          <String>[
+            '--enable-dart-profiling',
+            '--enable-service-port-fallback',
+            '--disable-service-auth-codes',
+            '--observatory-port=60700',
+            '--start-paused',
+            '--dart-flags="--foo"',
+            '--enable-checked-mode',
+            '--verify-entry-points',
+            '--enable-software-rendering',
+            '--skia-deterministic-rendering',
+            '--trace-skia',
+            '--endless-trace-buffer',
+            '--dump-skp-on-shader-compilation',
+            '--verbose-logging',
+            '--cache-sksl',
+          ].join(' '),
+        ], environment: const <String, String>{
+          'PATH': '/usr/bin:null',
+          'DYLD_LIBRARY_PATH': '/path/to/libraries',
+        }
+      )
+    ]);
+    final IOSDevice device = setUpIOSDevice(
+      sdkVersion: '13.3',
+      processManager: processManager,
+      fileSystem: fileSystem,
+    );
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      bundleName: 'Runner',
+      bundleDir: fileSystem.currentDirectory,
+    );
+    final Uri uri = Uri(
+      scheme: 'http',
+      host: '127.0.0.1',
+      port: 1234,
+      path: 'observatory',
+    );
+
+    device.setLogReader(iosApp, FakeDeviceLogReader());
+    device.portForwarder = const NoOpDevicePortForwarder();
+
+    when(MDnsObservatoryDiscovery.instance.getObservatoryUri(any, any, usesIpv6: anyNamed('usesIpv6')))
+      .thenAnswer((Invocation invocation) async => uri);
+
+    final LaunchResult launchResult = await device.startApp(iosApp,
+      prebuiltApplication: true,
+      debuggingOptions: DebuggingOptions.enabled(
+        BuildInfo.debug,
+        startPaused: true,
+        disableServiceAuthCodes: true,
+        dartFlags: '--foo',
+        enableSoftwareRendering: true,
+        skiaDeterministicRendering: true,
+        traceSkia: true,
+        traceSystrace: true,
+        endlessTraceBuffer: true,
+        dumpSkpOnShaderCompilation: true,
+        cacheSkSL: true,
+        verboseSystemLogs: true,
+      ),
+      platformArgs: <String, dynamic>{},
+    );
+
+    expect(launchResult.started, true);
+    expect(await device.stopApp(iosApp), false);
+    expect(processManager.hasRemainingExpectations, false);
+  }, overrides: <Type, Generator>{
+    MDnsObservatoryDiscovery: () => MockMDnsObservatoryDiscovery(),
+    Usage: () => MockUsage(),
+  });
+}
+
+IOSDevice setUpIOSDevice({
+  String sdkVersion = '13.0.1',
+  FileSystem fileSystem,
+  Logger logger,
+  ProcessManager processManager,
+}) {
+  const MapEntry<String, String> dyldLibraryEntry = MapEntry<String, String>(
+    'DYLD_LIBRARY_PATH',
+    '/path/to/libraries',
+  );
+  final MockCache cache = MockCache();
+  final MockArtifacts artifacts = MockArtifacts();
+  final FakePlatform macPlatform = FakePlatform(
+    operatingSystem: 'macos',
+    environment: <String, String>{},
+  );
+  when(cache.dyLdLibEntry).thenReturn(dyldLibraryEntry);
+  when(artifacts.getArtifactPath(Artifact.iosDeploy, platform: anyNamed('platform')))
+    .thenReturn('ios-deploy');
+  return IOSDevice('123',
+    name: 'iPhone 1',
+    sdkVersion: sdkVersion,
+    fileSystem: fileSystem ?? MemoryFileSystem.test(),
+    platform: macPlatform,
+    artifacts: artifacts,
+    logger: BufferLogger.test(),
+    iosDeploy: IOSDeploy(
+      logger: logger ?? BufferLogger.test(),
+      platform: macPlatform,
+      processManager: processManager ?? FakeProcessManager.any(),
+      artifacts: artifacts,
+      cache: cache,
+    ),
+    cpuArchitecture: DarwinArch.arm64,
+  );
+}
+
+class MockDevicePortForwarder extends Mock implements DevicePortForwarder {}
+class MockDeviceLogReader extends Mock implements DeviceLogReader  {}
+class MockUsage extends Mock implements Usage {}
+class MockMDnsObservatoryDiscovery extends Mock implements MDnsObservatoryDiscovery {}
+class MockArtifacts extends Mock implements Artifacts {}
+class MockCache extends Mock implements Cache {}
diff --git a/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart b/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart
index 983af77..70811d1 100644
--- a/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart
+++ b/packages/flutter_tools/test/general.shard/protocol_discovery_test.dart
@@ -10,11 +10,11 @@
 
 import '../src/common.dart';
 import '../src/context.dart';
-import '../src/mocks.dart';
+import '../src/fakes.dart';
 
 void main() {
   group('service_protocol discovery', () {
-    MockDeviceLogReader logReader;
+    FakeDeviceLogReader logReader;
     ProtocolDiscovery discoverer;
 
     /// Performs test set-up functionality that must be performed as part of
@@ -37,7 +37,7 @@
       int devicePort,
       Duration throttleDuration = const Duration(milliseconds: 200),
     }) {
-      logReader = MockDeviceLogReader();
+      logReader = FakeDeviceLogReader();
       discoverer = ProtocolDiscovery.observatory(
         logReader,
         ipv6: false,
@@ -261,7 +261,7 @@
 
     group('port forwarding', () {
       testUsingContext('default port', () async {
-        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final FakeDeviceLogReader logReader = FakeDeviceLogReader();
         final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
           logReader,
           portForwarder: MockPortForwarder(99),
@@ -282,7 +282,7 @@
       });
 
       testUsingContext('specified port', () async {
-        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final FakeDeviceLogReader logReader = FakeDeviceLogReader();
         final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
           logReader,
           portForwarder: MockPortForwarder(99),
@@ -303,7 +303,7 @@
       });
 
       testUsingContext('specified port zero', () async {
-        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final FakeDeviceLogReader logReader = FakeDeviceLogReader();
         final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
           logReader,
           portForwarder: MockPortForwarder(99),
@@ -324,7 +324,7 @@
       });
 
       testUsingContext('ipv6', () async {
-        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final FakeDeviceLogReader logReader = FakeDeviceLogReader();
         final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
           logReader,
           portForwarder: MockPortForwarder(99),
@@ -345,7 +345,7 @@
       });
 
       testUsingContext('ipv6 with Ascii Escape code', () async {
-        final MockDeviceLogReader logReader = MockDeviceLogReader();
+        final FakeDeviceLogReader logReader = FakeDeviceLogReader();
         final ProtocolDiscovery discoverer = ProtocolDiscovery.observatory(
           logReader,
           portForwarder: MockPortForwarder(99),
diff --git a/packages/flutter_tools/test/src/fake_process_manager.dart b/packages/flutter_tools/test/src/fake_process_manager.dart
index a0cebc4..a911c8e 100644
--- a/packages/flutter_tools/test/src/fake_process_manager.dart
+++ b/packages/flutter_tools/test/src/fake_process_manager.dart
@@ -82,39 +82,14 @@
   /// resolves.
   final Completer<void> completer;
 
-  static bool _listEquals<T>(List<T> a, List<T> b) {
-    if (a == null) {
-      return b == null;
-    }
-    if (b == null || a.length != b.length) {
-      return false;
-    }
-    for (int index = 0; index < a.length; index += 1) {
-      if (a[index] != b[index]) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  bool _matches(List<String> command, String workingDirectory, Map<String, String> environment) {
-    if (!_listEquals(command, this.command)) {
-      return false;
-    }
-    if (this.workingDirectory != null && workingDirectory != this.workingDirectory) {
-      return false;
+  void _matches(List<String> command, String workingDirectory, Map<String, String> environment) {
+    expect(command, equals(this.command));
+    if (this.workingDirectory != null) {
+      expect(this.workingDirectory, workingDirectory);
     }
     if (this.environment != null) {
-      if (environment == null) {
-        return false;
-      }
-      for (final String key in environment.keys) {
-        if (environment[key] != this.environment[key]) {
-          return false;
-        }
-      }
+      expect(this.environment, environment);
     }
-    return true;
   }
 }
 
@@ -322,12 +297,7 @@
       reason: 'ProcessManager was told to execute $command (in $workingDirectory) '
               'but the FakeProcessManager.list expected no more processes.'
     );
-    expect(_commands.first._matches(command, workingDirectory, environment), isTrue,
-      reason: 'ProcessManager was told to execute $command '
-              '(in $workingDirectory, with environment $environment) '
-              'but the next process that was expected was ${_commands.first.command} '
-              '(in ${_commands.first.workingDirectory}, with environment ${_commands.first.environment})}.'
-    );
+    _commands.first._matches(command, workingDirectory, environment);
     return _commands.removeAt(0);
   }
 
diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart
new file mode 100644
index 0000000..b2cc65e
--- /dev/null
+++ b/packages/flutter_tools/test/src/fakes.dart
@@ -0,0 +1,42 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter_tools/src/device.dart';
+
+/// A fake implementation of the [DeviceLogReader].
+class FakeDeviceLogReader extends DeviceLogReader {
+  @override
+  String get name => 'FakeLogReader';
+
+  StreamController<String> _cachedLinesController;
+
+  final List<String> _lineQueue = <String>[];
+  StreamController<String> get _linesController {
+    _cachedLinesController ??= StreamController<String>
+      .broadcast(onListen: () {
+        _lineQueue.forEach(_linesController.add);
+        _lineQueue.clear();
+     });
+    return _cachedLinesController;
+  }
+
+  @override
+  Stream<String> get logLines => _linesController.stream;
+
+  void addLine(String line) {
+    if (_linesController.hasListener) {
+      _linesController.add(line);
+    } else {
+      _lineQueue.add(line);
+    }
+  }
+
+  @override
+  Future<void> dispose() async {
+    _lineQueue.clear();
+    await _linesController.close();
+  }
+}
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 47808de..006b6b9 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -612,40 +612,6 @@
   bool isSupportedForProject(FlutterProject flutterProject) => true;
 }
 
-class MockDeviceLogReader extends DeviceLogReader {
-  @override
-  String get name => 'MockLogReader';
-
-  StreamController<String> _cachedLinesController;
-
-  final List<String> _lineQueue = <String>[];
-  StreamController<String> get _linesController {
-    _cachedLinesController ??= StreamController<String>
-      .broadcast(onListen: () {
-        _lineQueue.forEach(_linesController.add);
-        _lineQueue.clear();
-     });
-    return _cachedLinesController;
-  }
-
-  @override
-  Stream<String> get logLines => _linesController.stream;
-
-  void addLine(String line) {
-    if (_linesController.hasListener) {
-      _linesController.add(line);
-    } else {
-      _lineQueue.add(line);
-    }
-  }
-
-  @override
-  Future<void> dispose() async {
-    _lineQueue.clear();
-    await _linesController.close();
-  }
-}
-
 void applyMocksToCommand(FlutterCommand command) {
   command.applicationPackages = MockApplicationPackageStore();
 }