refactor cocoapods validator to detect broken install (#38560)

diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 1764483..a1f1d91 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -172,6 +172,11 @@
       '$consequence\n'
       'To upgrade:\n'
       '$upgradeInstructions';
+  String cocoaPodsBrokenInstall(String consequence, String reinstallInstructions) =>
+      'CocoaPods installed but not working.\n'
+      '$consequence\n'
+      'To re-install CocoaPods, run:\n'
+      '$reinstallInstructions';
 
   // Messages used in VsCodeValidator
   String vsCodeVersion(String version) => 'version $version';
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart
index ca2577f..ac006fa 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart
@@ -29,6 +29,11 @@
   Flutter is unable to determine the installed CocoaPods's version.
   Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.''';
 
+const String brokenCocoaPodsConsequence = '''
+  You appear to have CocoaPods installed but it is not working.
+  This can happen if the version of Ruby that CocoaPods was installed with is different from the one being used to invoke it.
+  This can usually be fixed by re-installing CocoaPods. For more info, see https://github.com/flutter/flutter/issues/14293.''';
+
 const String cocoaPodsInstallInstructions = '''
   sudo gem install cocoapods
   pod setup''';
@@ -52,6 +57,8 @@
   belowRecommendedVersion,
   /// Everything should be fine.
   recommended,
+  /// iOS plugins will not work, re-install required.
+  brokenInstall,
 }
 
 class CocoaPods {
@@ -60,6 +67,8 @@
   String get cocoaPodsMinimumVersion => '1.6.0';
   String get cocoaPodsRecommendedVersion => '1.6.0';
 
+  Future<bool> get isInstalled => exitsHappyAsync(<String>['which', 'pod']);
+
   Future<String> get cocoaPodsVersionText {
     _versionText ??= runAsync(<String>['pod', '--version']).then<String>((RunResult result) {
       return result.exitCode == 0 ? result.stdout.trim() : null;
@@ -68,9 +77,13 @@
   }
 
   Future<CocoaPodsStatus> get evaluateCocoaPodsInstallation async {
-    final String versionText = await cocoaPodsVersionText;
-    if (versionText == null)
+    if (!(await isInstalled)) {
       return CocoaPodsStatus.notInstalled;
+    }
+    final String versionText = await cocoaPodsVersionText;
+    if (versionText == null) {
+      return CocoaPodsStatus.brokenInstall;
+    }
     try {
       final Version installedVersion = Version.parse(versionText);
       if (installedVersion == null)
diff --git a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
index aaee745..ce57632 100644
--- a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
+++ b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart
@@ -34,6 +34,11 @@
         status = ValidationType.missing;
         messages.add(ValidationMessage.error(
             userMessages.cocoaPodsMissing(noCocoaPodsConsequence, cocoaPodsInstallInstructions)));
+      } else if (cocoaPodsStatus == CocoaPodsStatus.brokenInstall) {
+        status = ValidationType.missing;
+        messages.add(ValidationMessage.error(
+            userMessages.cocoaPodsBrokenInstall(brokenCocoaPodsConsequence, cocoaPodsInstallInstructions)));
+
       } else if (cocoaPodsStatus == CocoaPodsStatus.unknownVersion) {
         status = ValidationType.partial;
         messages.add(ValidationMessage.hint(
diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
index 068b87a..6ebccdb 100644
--- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
@@ -30,10 +30,6 @@
   CocoaPods cocoaPodsUnderTest;
   InvokeProcess resultOfPodVersion;
 
-  void pretendPodIsNotInstalled() {
-    resultOfPodVersion = () async => throw 'Executable does not exist';
-  }
-
   void pretendPodVersionFails() {
     resultOfPodVersion = () async => exitsWithError();
   }
@@ -93,6 +89,22 @@
     )).thenAnswer((_) async => exitsHappy());
   });
 
+  void pretendPodIsNotInstalled() {
+    when(mockProcessManager.run(
+      <String>['which', 'pod'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenAnswer((_) async => exitsWithError());
+  }
+
+  void pretendPodIsInstalled() {
+    when(mockProcessManager.run(
+      <String>['which', 'pod'],
+      workingDirectory: anyNamed('workingDirectory'),
+      environment: anyNamed('environment'),
+    )).thenAnswer((_) async => exitsHappy());
+  }
+
   group('Evaluate installation', () {
     testUsingContext('detects not installed, if pod exec does not exist', () async {
       pretendPodIsNotInstalled();
@@ -101,14 +113,16 @@
       ProcessManager: () => mockProcessManager,
     });
 
-    testUsingContext('detects not installed, if pod version fails', () async {
+    testUsingContext('detects not installed, if pod is installed but version fails', () async {
+      pretendPodIsInstalled();
       pretendPodVersionFails();
-      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
+      expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.brokenInstall);
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
     });
 
     testUsingContext('detects installed', () async {
+      pretendPodIsInstalled();
       pretendPodVersionIs('0.0.1');
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, isNot(CocoaPodsStatus.notInstalled));
     }, overrides: <Type, Generator>{
@@ -116,6 +130,7 @@
     });
 
     testUsingContext('detects unknown version', () async {
+      pretendPodIsInstalled();
       pretendPodVersionIs('Plugin loaded.\n1.5.3');
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.unknownVersion);
     }, overrides: <Type, Generator>{
@@ -123,6 +138,7 @@
     });
 
     testUsingContext('detects below minimum version', () async {
+      pretendPodIsInstalled();
       pretendPodVersionIs('1.5.0');
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.belowMinimumVersion);
     }, overrides: <Type, Generator>{
@@ -130,6 +146,7 @@
     });
 
     testUsingContext('detects at recommended version', () async {
+      pretendPodIsInstalled();
       pretendPodVersionIs('1.6.0');
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
     }, overrides: <Type, Generator>{
@@ -137,6 +154,7 @@
     });
 
     testUsingContext('detects above recommended version', () async {
+      pretendPodIsInstalled();
       pretendPodVersionIs('1.6.1');
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.recommended);
     }, overrides: <Type, Generator>{
@@ -279,6 +297,7 @@
     });
 
     testUsingContext('throws, if Podfile is missing.', () async {
+      pretendPodIsInstalled();
       try {
         await cocoaPodsUnderTest.processPods(
           xcodeProject: projectUnderTest.ios,
@@ -299,6 +318,7 @@
     });
 
     testUsingContext('throws, if specs repo is outdated.', () async {
+      pretendPodIsInstalled();
       fs.file(fs.path.join('project', 'ios', 'Podfile'))
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -345,6 +365,7 @@
     });
 
     testUsingContext('run pod install, if Podfile.lock is missing', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -368,6 +389,7 @@
     });
 
     testUsingContext('runs pod install, if Manifest.lock is missing', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -394,6 +416,7 @@
     });
 
     testUsingContext('runs pod install, if Manifest.lock different from Podspec.lock', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -423,6 +446,7 @@
     });
 
     testUsingContext('runs pod install, if flutter framework changed', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -452,6 +476,7 @@
     });
 
     testUsingContext('runs pod install, if Podfile.lock is older than Podfile', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -483,6 +508,7 @@
     });
 
     testUsingContext('skips pod install, if nothing changed', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -509,6 +535,7 @@
     });
 
     testUsingContext('a failed pod install deletes Pods/Manifest.lock', () async {
+      pretendPodIsInstalled();
       projectUnderTest.ios.podfile
         ..createSync()
         ..writeAsStringSync('Existing Podfile');
@@ -559,6 +586,7 @@
     });
 
     testUsingContext('succeeds, if specs repo is in CP_REPOS_DIR.', () async {
+      pretendPodIsInstalled();
       fs.file(fs.path.join('project', 'ios', 'Podfile'))
         ..createSync()
         ..writeAsStringSync('Existing Podfile');