[flutter tools] rewrite launch non-prebuilt app tests (#53351)

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 84d9f6e..a5fa58d 100644
--- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -3,11 +3,12 @@
 // found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert';
 
-import 'package:args/command_runner.dart';
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.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';
@@ -15,26 +16,15 @@
 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/commands/create.dart';
 import 'package:flutter_tools/src/device.dart';
-import 'package:flutter_tools/src/doctor.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
-import 'package:flutter_tools/src/project.dart';
-import 'package:flutter_tools/src/globals.dart' as globals;
-
-import 'package:meta/meta.dart';
-import 'package:mockito/mockito.dart';
-import 'package:platform/platform.dart';
-import 'package:process/process.dart';
-import 'package:quiver/testing/async.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
-import '../../src/fakes.dart';
 import '../../src/mocks.dart';
 
 void main() {
@@ -59,7 +49,6 @@
       mockCache = MockCache();
       const MapEntry<String, String> dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', '/path/to/libs');
       when(mockCache.dyLdLibEntry).thenReturn(dyLdLibEntry);
-      mockFileSystem = MockFileSystem();
       logger = BufferLogger.test();
       iosDeploy = IOSDeploy(
         artifacts: mockArtifacts,
@@ -231,7 +220,6 @@
         forwardedPort = ForwardedPort.withContext(123, 456, mockProcess3);
         mockArtifacts = MockArtifacts();
         mockCache = MockCache();
-        mockFileSystem = MockFileSystem();
         iosDeploy = IOSDeploy(
           artifacts: mockArtifacts,
           cache: mockCache,
@@ -268,271 +256,12 @@
         verify(mockProcess3.kill());
       });
     });
-
-    group('startApp', () {
-      MockIOSApp mockApp;
-      MockArtifacts mockArtifacts;
-      MockCache mockCache;
-      MockFileSystem mockFileSystem;
-      MockPlatform mockPlatform;
-      MockProcessManager mockProcessManager;
-      FakeDeviceLogReader mockLogReader;
-      MockPortForwarder mockPortForwarder;
-      MockIMobileDevice mockIMobileDevice;
-      MockIOSDeploy mockIosDeploy;
-
-      Directory tempDir;
-      Directory projectDir;
-
-      const int devicePort = 499;
-      const int hostPort = 42;
-      const String installerPath = '/path/to/ideviceinstaller';
-      const String iosDeployPath = '/path/to/iosdeploy';
-      const String iproxyPath = '/path/to/iproxy';
-      const MapEntry<String, String> libraryEntry = MapEntry<String, String>(
-          'DYLD_LIBRARY_PATH',
-          '/path/to/libraries',
-      );
-      final Map<String, String> env = Map<String, String>.fromEntries(
-          <MapEntry<String, String>>[libraryEntry]
-      );
-
-      setUp(() {
-        Cache.disableLocking();
-
-        mockApp = MockIOSApp();
-        mockArtifacts = MockArtifacts();
-        mockCache = MockCache();
-        when(mockCache.dyLdLibEntry).thenReturn(libraryEntry);
-        mockFileSystem = MockFileSystem();
-        mockPlatform = MockPlatform();
-        when(mockPlatform.isMacOS).thenReturn(true);
-        mockProcessManager = MockProcessManager();
-        mockLogReader = FakeDeviceLogReader();
-        mockPortForwarder = MockPortForwarder();
-        mockIMobileDevice = MockIMobileDevice();
-        mockIosDeploy = MockIOSDeploy();
-
-        tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_tools_create_test.');
-        projectDir = tempDir.childDirectory('flutter_project');
-
-        when(
-            mockArtifacts.getArtifactPath(
-                Artifact.ideviceinstaller,
-                platform: anyNamed('platform'),
-            ),
-        ).thenReturn(installerPath);
-
-        when(
-            mockArtifacts.getArtifactPath(
-                Artifact.iosDeploy,
-                platform: anyNamed('platform'),
-            ),
-        ).thenReturn(iosDeployPath);
-
-        when(
-            mockArtifacts.getArtifactPath(
-                Artifact.iproxy,
-                platform: anyNamed('platform'),
-            ),
-        ).thenReturn(iproxyPath);
-
-        when(mockPortForwarder.forward(devicePort, hostPort: anyNamed('hostPort')))
-          .thenAnswer((_) async => hostPort);
-        when(mockPortForwarder.forwardedPorts)
-          .thenReturn(<ForwardedPort>[ForwardedPort(hostPort, devicePort)]);
-        when(mockPortForwarder.unforward(any))
-          .thenAnswer((_) async => null);
-
-        final MemoryFileSystem memoryFileSystem = MemoryFileSystem();
-        when(mockFileSystem.currentDirectory)
-          .thenReturn(memoryFileSystem.currentDirectory);
-
-        const String bundlePath = '/path/to/bundle';
-        final List<String> installArgs = <String>[installerPath, '-i', bundlePath];
-        when(mockApp.deviceBundlePath).thenReturn(bundlePath);
-        final MockDirectory directory = MockDirectory();
-        when(mockFileSystem.directory(bundlePath)).thenReturn(directory);
-        when(directory.existsSync()).thenReturn(true);
-        when(mockProcessManager.run(
-          installArgs,
-          workingDirectory: anyNamed('workingDirectory'),
-          environment: env,
-        )).thenAnswer(
-          (_) => Future<ProcessResult>.value(ProcessResult(1, 0, '', ''))
-        );
-      });
-
-      tearDown(() {
-        mockLogReader.dispose();
-        tryToDelete(tempDir);
-
-        Cache.enableLocking();
-      });
-
-      void testNonPrebuilt(
-        String name, {
-        @required bool showBuildSettingsFlakes,
-        void Function() additionalSetup,
-        void Function() additionalExpectations,
-      }) {
-        testUsingContext('non-prebuilt succeeds in debug mode $name', () async {
-          final Directory targetBuildDir =
-              projectDir.childDirectory('build/ios/iphoneos/Debug-arm64');
-
-          // The -showBuildSettings calls have a timeout and so go through
-          // globals.processManager.start().
-          mockProcessManager.processFactory = flakyProcessFactory(
-            flakes: showBuildSettingsFlakes ? 1 : 0,
-            delay: const Duration(seconds: 62),
-            filter: (List<String> args) => args.contains('-showBuildSettings'),
-            stdout:
-                () => Stream<String>
-                  .fromIterable(
-                      <String>['TARGET_BUILD_DIR = ${targetBuildDir.path}\n'])
-                  .transform(utf8.encoder),
-          );
-
-          // Make all other subcommands succeed.
-          when(mockProcessManager.run(
-              any,
-              workingDirectory: anyNamed('workingDirectory'),
-              environment: anyNamed('environment'),
-          )).thenAnswer((Invocation inv) {
-            return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
-          });
-
-          when(mockProcessManager.run(
-            argThat(contains('find-identity')),
-            environment: anyNamed('environment'),
-            workingDirectory: anyNamed('workingDirectory'),
-          )).thenAnswer((_) => Future<ProcessResult>.value(ProcessResult(
-                1, // pid
-                0, // exitCode
-                '''
-    1) 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 "iPhone Developer: Profile 1 (1111AAAA11)"
-    2) da4b9237bacccdf19c0760cab7aec4a8359010b0 "iPhone Developer: Profile 2 (2222BBBB22)"
-    3) 5bf1fd927dfb8679496a2e6cf00cbe50c1c87145 "iPhone Developer: Profile 3 (3333CCCC33)"
-        3 valid identities found''',
-                '',
-          )));
-
-          // Deploy works.
-          when(mockIosDeploy.runApp(
-            deviceId: anyNamed('deviceId'),
-            bundlePath: anyNamed('bundlePath'),
-            launchArguments: anyNamed('launchArguments'),
-          )).thenAnswer((_) => Future<int>.value(0));
-
-          // Create a dummy project to avoid mocking out the whole directory
-          // structure expected by device.startApp().
-          Cache.flutterRoot = '../..';
-          final CreateCommand command = CreateCommand();
-          final CommandRunner<void> runner = createTestCommandRunner(command);
-          await runner.run(<String>[
-            'create',
-            '--no-pub',
-            projectDir.path,
-          ]);
-
-          if (additionalSetup != null) {
-            additionalSetup();
-          }
-
-          final IOSApp app = await AbsoluteBuildableIOSApp.fromProject(
-            FlutterProject.fromDirectory(projectDir).ios);
-          final IOSDevice device = IOSDevice(
-            '123',
-            name: 'iPhone 1',
-            sdkVersion: '13.3',
-            artifacts: mockArtifacts,
-            fileSystem: globals.fs,
-            logger: testLogger,
-            platform: macPlatform,
-            iosDeploy: mockIosDeploy,
-            iMobileDevice: iMobileDevice,
-            cpuArchitecture: DarwinArch.arm64,
-          );
-
-          // Pre-create the expected build products.
-          targetBuildDir.createSync(recursive: true);
-          projectDir.childDirectory('build/ios/iphoneos/Runner.app').createSync(recursive: true);
-
-          final Completer<LaunchResult> completer = Completer<LaunchResult>();
-          FakeAsync().run((FakeAsync time) {
-            device.startApp(
-              app,
-              prebuiltApplication: false,
-              debuggingOptions: DebuggingOptions.disabled(const BuildInfo(BuildMode.debug, null, treeShakeIcons: false)),
-              platformArgs: <String, dynamic>{},
-            ).then((LaunchResult result) {
-              completer.complete(result);
-            });
-            time.flushMicrotasks();
-            time.elapse(const Duration(seconds: 65));
-          });
-          final LaunchResult launchResult = await completer.future;
-          expect(launchResult.started, isTrue);
-          expect(launchResult.hasObservatory, isFalse);
-          expect(await device.stopApp(mockApp), isFalse);
-
-          if (additionalExpectations != null) {
-            additionalExpectations();
-          }
-        }, overrides: <Type, Generator>{
-          DoctorValidatorsProvider: () => FakeIosDoctorProvider(),
-          IMobileDevice: () => mockIMobileDevice,
-          Platform: () => macPlatform,
-          ProcessManager: () => mockProcessManager,
-        });
-      }
-
-      testNonPrebuilt('flaky: false', showBuildSettingsFlakes: false);
-      testNonPrebuilt('flaky: true', showBuildSettingsFlakes: true);
-      testNonPrebuilt('with concurrent build failiure',
-        showBuildSettingsFlakes: false,
-        additionalSetup: () {
-          int callCount = 0;
-          when(mockProcessManager.run(
-            argThat(allOf(
-              contains('xcodebuild'),
-              contains('-configuration'),
-              contains('Debug'),
-            )),
-            workingDirectory: anyNamed('workingDirectory'),
-            environment: anyNamed('environment'),
-          )).thenAnswer((Invocation inv) {
-            // Succeed after 2 calls.
-            if (++callCount > 2) {
-              return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
-            }
-            // Otherwise fail with the Xcode concurrent error.
-            return Future<ProcessResult>.value(ProcessResult(
-              0,
-              1,
-              '''
-                "/Developer/Xcode/DerivedData/foo/XCBuildData/build.db":
-                database is locked
-                Possibly there are two concurrent builds running in the same filesystem location.
-                ''',
-              '',
-            ));
-          });
-        },
-        additionalExpectations: () {
-          expect(testLogger.statusText, contains('will retry in 2 seconds'));
-          expect(testLogger.statusText, contains('will retry in 4 seconds'));
-          expect(testLogger.statusText, contains('Xcode build done.'));
-        },
-      );
-    });
   });
 
   group('pollingGetDevices', () {
     MockXcdevice mockXcdevice;
     MockArtifacts mockArtifacts;
     MockCache mockCache;
-    MockFileSystem mockFileSystem;
     FakeProcessManager fakeProcessManager;
     Logger logger;
     IOSDeploy iosDeploy;
@@ -544,7 +273,6 @@
       mockArtifacts = MockArtifacts();
       mockCache = MockCache();
       logger = BufferLogger.test();
-      mockFileSystem = MockFileSystem();
       mockIosWorkflow = MockIOSWorkflow();
       fakeProcessManager = FakeProcessManager.any();
       iosDeploy = IOSDeploy(
@@ -596,7 +324,7 @@
         iMobileDevice: iMobileDevice,
         logger: logger,
         platform: macPlatform,
-        fileSystem: mockFileSystem,
+        fileSystem: MemoryFileSystem.test(),
       );
       when(mockXcdevice.getAvailableTetheredIOSDevices())
           .thenAnswer((Invocation invocation) => Future<List<IOSDevice>>.value(<IOSDevice>[device]));
@@ -646,48 +374,10 @@
   });
 }
 
-class AbsoluteBuildableIOSApp extends BuildableIOSApp {
-  AbsoluteBuildableIOSApp(IosProject project, String projectBundleId) :
-    super(project, projectBundleId);
-
-  static Future<AbsoluteBuildableIOSApp> fromProject(IosProject project) async {
-    final String projectBundleId = await project.productBundleIdentifier;
-    return AbsoluteBuildableIOSApp(project, projectBundleId);
-  }
-
-  @override
-  String get deviceBundlePath =>
-      globals.fs.path.join(project.parent.directory.path, 'build', 'ios', 'iphoneos', name);
-
-}
-
-class FakeIosDoctorProvider implements DoctorValidatorsProvider {
-  List<Workflow> _workflows;
-
-  @override
-  List<DoctorValidator> get validators => <DoctorValidator>[];
-
-  @override
-  List<Workflow> get workflows {
-    if (_workflows == null) {
-      _workflows = <Workflow>[];
-      if (globals.iosWorkflow.appliesToHostPlatform) {
-        _workflows.add(globals.iosWorkflow);
-      }
-    }
-    return _workflows;
-  }
-}
-
 class MockIOSApp extends Mock implements IOSApp {}
 class MockArtifacts extends Mock implements Artifacts {}
 class MockCache extends Mock implements Cache {}
-class MockDirectory extends Mock implements Directory {}
-class MockFile extends Mock implements File {}
-class MockFileSystem extends Mock implements FileSystem {}
 class MockIMobileDevice extends Mock implements IMobileDevice {}
 class MockIOSDeploy extends Mock implements IOSDeploy {}
 class MockIOSWorkflow extends Mock implements IOSWorkflow {}
-class MockPlatform extends Mock implements Platform {}
-class MockPortForwarder extends Mock implements DevicePortForwarder {}
 class MockXcdevice extends Mock implements XCDevice {}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
new file mode 100644
index 0000000..e82ed73
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -0,0 +1,300 @@
+// 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 '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/ios/mac.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:quiver/testing/async.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/fake_process_manager.dart';
+
+const List<String> kRunReleaseArgs = <String>[
+  '/usr/bin/env',
+  'xcrun',
+  'xcodebuild',
+  '-configuration',
+  'Release',
+  '-quiet',
+  '-workspace',
+  'Runner.xcworkspace',
+  '-scheme',
+  'Runner',
+  'BUILD_DIR=/build/ios',
+  '-sdk',
+  'iphoneos',
+  'ONLY_ACTIVE_ARCH=YES',
+  'ARCHS=arm64',
+  'FLUTTER_SUPPRESS_ANALYTICS=true',
+  'COMPILER_INDEX_STORE_ENABLE=NO',
+];
+
+const String kConcurrentBuildErrorMessage = '''
+"/Developer/Xcode/DerivedData/foo/XCBuildData/build.db":
+database is locked
+Possibly there are two concurrent builds running in the same filesystem location.
+''';
+
+final FakePlatform macPlatform = FakePlatform(
+  operatingSystem: 'macos',
+  environment: <String, String>{},
+);
+
+void main() {
+  FileSystem fileSystem;
+  FakeProcessManager processManager;
+  BufferLogger logger;
+
+  setUp(() {
+    logger = BufferLogger.test();
+    fileSystem = MemoryFileSystem.test();
+    processManager = FakeProcessManager.list(<FakeCommand>[]);
+  });
+
+  testUsingContext('IOSDevice.startApp succeeds in release mode with buildable app', () async {
+    final IOSDevice iosDevice = setUpIOSDevice(
+      fileSystem: fileSystem,
+      processManager: processManager,
+      logger: logger,
+    );
+    setUpIOSProject(fileSystem);
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+    final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
+
+    processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
+    processManager.addCommand(const FakeCommand(command: <String>[...kRunReleaseArgs, '-showBuildSettings']));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        'ios-deploy',
+        '--id',
+        '123',
+        '--bundle',
+        'build/ios/iphoneos/Runner.app',
+        '--no-wifi',
+        '--justlaunch',
+        '--args',
+        const <String>[
+          '--enable-dart-profiling',
+          '--enable-service-port-fallback',
+          '--disable-service-auth-codes',
+          '--observatory-port=53781',
+        ].join(' ')
+      ])
+    );
+
+    final LaunchResult launchResult = await iosDevice.startApp(
+      buildableIOSApp,
+      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+      platformArgs: <String, Object>{},
+    );
+
+    expect(launchResult.started, true);
+    expect(processManager.hasRemainingExpectations, false);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => processManager,
+    FileSystem: () => fileSystem,
+    Logger: () => logger,
+    Platform: () => macPlatform,
+  });
+
+  testUsingContext('IOSDevice.startApp succeeds in release mode with buildable '
+    'app with flaky buildSettings call', () async {
+    LaunchResult launchResult;
+    FakeAsync().run((FakeAsync time) {
+      final IOSDevice iosDevice = setUpIOSDevice(
+        fileSystem: fileSystem,
+        processManager: processManager,
+        logger: logger,
+      );
+      setUpIOSProject(fileSystem);
+      final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+      final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
+
+      processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
+      // The first showBuildSettings call should timeout.
+      processManager.addCommand(
+        const FakeCommand(
+          command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
+          duration: Duration(minutes: 5), // this is longer than the timeout of 1 minute.
+      ));
+      // The second call succeedes and is made after the first times out.
+      processManager.addCommand(
+        const FakeCommand(
+          command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
+          exitCode: 0,
+      ));
+      processManager.addCommand(FakeCommand(
+        command: <String>[
+          'ios-deploy',
+          '--id',
+          '123',
+          '--bundle',
+          'build/ios/iphoneos/Runner.app',
+          '--no-wifi',
+          '--justlaunch',
+          '--args',
+          const <String>[
+            '--enable-dart-profiling',
+            '--enable-service-port-fallback',
+            '--disable-service-auth-codes',
+            '--observatory-port=53781',
+          ].join(' ')
+        ])
+      );
+
+      iosDevice.startApp(
+        buildableIOSApp,
+        debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+        platformArgs: <String, Object>{},
+      ).then((LaunchResult result) {
+        launchResult = result;
+      });
+
+      // Elapse duration for process timeout.
+      time.flushMicrotasks();
+      time.elapse(const Duration(minutes: 1));
+
+      // Elapse duration for overall process timer.
+      time.flushMicrotasks();
+      time.elapse(const Duration(minutes: 5));
+
+      time.flushTimers();
+    });
+
+    expect(launchResult?.started, true);
+    expect(processManager.hasRemainingExpectations, false);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => processManager,
+    FileSystem: () => fileSystem,
+    Logger: () => logger,
+    Platform: () => macPlatform,
+  });
+
+  testUsingContext('IOSDevice.startApp succeeds in release mode with buildable '
+    'app with concurrent build failure', () async {
+    final IOSDevice iosDevice = setUpIOSDevice(
+      fileSystem: fileSystem,
+      processManager: processManager,
+      logger: logger,
+    );
+    setUpIOSProject(fileSystem);
+    final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+    final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter');
+
+    // The first xcrun call should fail with a
+    // concurrent build exception.
+    processManager.addCommand(
+      const FakeCommand(
+        command: kRunReleaseArgs,
+        exitCode: 1,
+        stdout: kConcurrentBuildErrorMessage,
+    ));
+    processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
+    processManager.addCommand(
+      const FakeCommand(
+        command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
+        exitCode: 0,
+    ));
+    processManager.addCommand(FakeCommand(
+      command: <String>[
+        'ios-deploy',
+        '--id',
+        '123',
+        '--bundle',
+        'build/ios/iphoneos/Runner.app',
+        '--no-wifi',
+        '--justlaunch',
+        '--args',
+        const <String>[
+          '--enable-dart-profiling',
+          '--enable-service-port-fallback',
+          '--disable-service-auth-codes',
+          '--observatory-port=53781',
+        ].join(' ')
+      ])
+    );
+
+    final LaunchResult launchResult = await iosDevice.startApp(
+      buildableIOSApp,
+      debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+      platformArgs: <String, Object>{},
+    );
+
+    expect(logger.statusText,
+      contains('Xcode build failed due to concurrent builds, will retry in 2 seconds'));
+    expect(launchResult.started, true);
+    expect(processManager.hasRemainingExpectations, false);
+  }, overrides: <Type, Generator>{
+    ProcessManager: () => processManager,
+    FileSystem: () => fileSystem,
+    Logger: () => logger,
+    Platform: () => macPlatform,
+  });
+}
+
+void setUpIOSProject(FileSystem fileSystem) {
+  fileSystem.file('pubspec.yaml').createSync();
+  fileSystem.file('.packages').writeAsStringSync('\n');
+  fileSystem.directory('ios').createSync();
+  fileSystem.directory('ios/Runner.xcworkspace').createSync();
+  fileSystem.directory('ios/Runner.xcodeproj').createSync();
+  fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync();
+  // This is the expected output directory.
+  fileSystem.directory('build/ios/iphoneos/Runner.app').createSync(recursive: true);
+}
+
+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();
+  logger ??= BufferLogger.test();
+  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: logger,
+    iosDeploy: IOSDeploy(
+      logger: logger,
+      platform: macPlatform,
+      processManager: processManager ?? FakeProcessManager.any(),
+      artifacts: artifacts,
+      cache: cache,
+    ),
+    iMobileDevice: IMobileDevice(
+      logger: logger,
+      processManager: processManager ?? FakeProcessManager.any(),
+      artifacts: artifacts,
+      cache: cache,
+    ),
+    cpuArchitecture: DarwinArch.arm64,
+  );
+}
+
+class MockArtifacts extends Mock implements Artifacts {}
+class MockCache extends Mock implements Cache {}