Fail ios workflow when simctl does not work (#15628)

* fail ios workflow when simctl does not work

* missed rename commit

* address comments
diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
index 55b5ed3..d561ff0 100644
--- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart
+++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart
@@ -23,7 +23,7 @@
 
   // We need xcode (+simctl) to list simulator devices, and libimobiledevice to list real devices.
   @override
-  bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck;
+  bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck && xcode.isSimctlInstalled;
 
   // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
   // for real devices.
@@ -87,6 +87,13 @@
           'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.'
         ));
       }
+      if (!xcode.isSimctlInstalled) {
+        xcodeStatus = ValidationType.partial;
+        messages.add(new ValidationMessage.error(
+          'Xcode requires additional components to be installed in order to run.\n'
+          'Launch Xcode and install additional required components when prompted.'
+        ));
+      }
       if ((await macDevMode).contains('disabled')) {
         xcodeStatus = ValidationType.partial;
         messages.add(new ValidationMessage.error(
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index 29bf8c8..aff5667 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -149,6 +149,23 @@
     return _eulaSigned;
   }
 
+  bool _isSimctlInstalled;
+
+  /// Verifies that simctl is installed by trying to run it.
+  bool get isSimctlInstalled {
+    if (_isSimctlInstalled == null) {
+      try {
+        // This command will error if additional components need to be installed in
+        // xcode 9.2 and above.
+        final ProcessResult result = processManager.runSync(<String>['/usr/bin/xcrun', 'simctl', 'list']);
+        _isSimctlInstalled = result.stderr == null;
+      } on ProcessException {
+        _isSimctlInstalled = false;
+      }
+    }
+    return _isSimctlInstalled;
+  }
+
   bool get isVersionSatisfactory {
     if (!xcodeProjectInterpreter.isInstalled)
       return false;
diff --git a/packages/flutter_tools/test/ios/ios_workflow_test.dart b/packages/flutter_tools/test/ios/ios_workflow_test.dart
index 401947a..29c1ad6 100644
--- a/packages/flutter_tools/test/ios/ios_workflow_test.dart
+++ b/packages/flutter_tools/test/ios/ios_workflow_test.dart
@@ -83,6 +83,7 @@
           .thenReturn('Xcode 7.0.1\nBuild version 7C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(false);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -98,6 +99,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(false);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -113,6 +115,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(macDevMode: 'Developer mode is currently disabled.');
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -128,6 +131,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasPythonSixModule: false);
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -143,6 +147,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasHomebrew: false);
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -158,6 +163,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -173,6 +179,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -187,6 +194,7 @@
       when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasIosDeploy: false);
       final ValidationResult result = await workflow.validate();
@@ -203,6 +211,7 @@
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0');
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -220,6 +229,7 @@
       when(xcode.eulaSigned).thenReturn(true);
       when(cocoaPods.isCocoaPodsInstalledAndMeetsVersionCheck).thenReturn(false);
       when(cocoaPods.hasCocoaPods).thenReturn(false);
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -238,6 +248,7 @@
       when(cocoaPods.isCocoaPodsInstalledAndMeetsVersionCheck).thenReturn(false);
       when(cocoaPods.hasCocoaPods).thenReturn(true);
       when(cocoaPods.cocoaPodsVersionText).thenReturn('0.39.0');
+      when(xcode.isSimctlInstalled).thenReturn(true);
       final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
       final ValidationResult result = await workflow.validate();
       expect(result.type, ValidationType.partial);
@@ -256,6 +267,7 @@
       when(cocoaPods.isCocoaPodsInstalledAndMeetsVersionCheck).thenReturn(false);
       when(cocoaPods.hasCocoaPods).thenReturn(true);
       when(cocoaPods.isCocoaPodsInitialized).thenReturn(false);
+      when(xcode.isSimctlInstalled).thenReturn(true);
 
       final ValidationResult result = await new IOSWorkflowTestTarget().validate();
       expect(result.type, ValidationType.partial);
@@ -267,12 +279,30 @@
       ProcessManager: () => processManager,
     });
 
+    testUsingContext('Emits partial status when simctl is not installed', () async {
+      when(xcode.isInstalled).thenReturn(true);
+      when(xcode.versionText)
+          .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
+      when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
+      when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(false);
+      final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget();
+      final ValidationResult result = await workflow.validate();
+      expect(result.type, ValidationType.partial);
+    }, overrides: <Type, Generator>{
+      IMobileDevice: () => iMobileDevice,
+      Xcode: () => xcode,
+      CocoaPods: () => cocoaPods,
+    });
+
+
     testUsingContext('Succeeds when all checks pass', () async {
       when(xcode.isInstalled).thenReturn(true);
       when(xcode.versionText)
           .thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
       when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
       when(xcode.eulaSigned).thenReturn(true);
+      when(xcode.isSimctlInstalled).thenReturn(true);
 
       ensureDirectoryExists(fs.path.join(homeDirPath, '.cocoapods', 'repos', 'master', 'README.md'));