Refactor exits happy (#52916)

diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart
index 3ceff0c..ea4e7ef 100644
--- a/packages/flutter_tools/lib/src/base/process.dart
+++ b/packages/flutter_tools/lib/src/base/process.dart
@@ -537,14 +537,16 @@
     Map<String, String> environment,
   }) {
     _traceCommand(cli);
+    if (!_processManager.canRun(cli.first)) {
+      _logger.printTrace('$cli either does not exist or is not executable.');
+      return false;
+    }
+
     try {
       return _processManager.runSync(cli, environment: environment).exitCode == 0;
     } on Exception catch (error) {
       _logger.printTrace('$cli failed with $error');
       return false;
-    } on ArgumentError catch (error) {
-      _logger.printTrace('$cli failed with $error');
-      return false;
     }
   }
 
@@ -554,14 +556,16 @@
     Map<String, String> environment,
   }) async {
     _traceCommand(cli);
+    if (!_processManager.canRun(cli.first)) {
+      _logger.printTrace('$cli either does not exist or is not executable.');
+      return false;
+    }
+
     try {
       return (await _processManager.run(cli, environment: environment)).exitCode == 0;
     } on Exception catch (error) {
       _logger.printTrace('$cli failed with $error');
       return false;
-    } on ArgumentError catch (error) {
-      _logger.printTrace('$cli failed with $error');
-      return false;
     }
   }
 
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index f506acd..7bdfee0 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -35,18 +35,7 @@
   final String _idevicesyslogPath;
   final String _idevicescreenshotPath;
 
-  bool get isInstalled {
-    _isInstalled ??= processUtils.exitsHappySync(
-      <String>[
-        _idevicescreenshotPath,
-        '-h',
-      ],
-      environment: Map<String, String>.fromEntries(
-        <MapEntry<String, String>>[globals.cache.dyLdLibEntry]
-      ),
-    );
-    return _isInstalled;
-  }
+  bool get isInstalled => _isInstalled ??= globals.processManager.canRun(_idevicescreenshotPath);
   bool _isInstalled;
 
   /// Starts `idevicesyslog` and returns the running process.
diff --git a/packages/flutter_tools/test/general.shard/base/process_test.dart b/packages/flutter_tools/test/general.shard/base/process_test.dart
index 7d5b17a..27569b1 100644
--- a/packages/flutter_tools/test/general.shard/base/process_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/process_test.dart
@@ -336,7 +336,7 @@
   });
 
   group('exitsHappySync', () {
-    ProcessManager mockProcessManager;
+    MockProcessManager mockProcessManager;
     ProcessUtils processUtils;
 
     setUp(() {
@@ -368,16 +368,27 @@
       expect(processUtils.exitsHappySync(<String>['boohoo']), isFalse);
     });
 
-    testWithoutContext('catches ArgumentError and returns false', () {
-      when(mockProcessManager.runSync(<String>['nonesuch'])).thenThrow(
-        ArgumentError('Invalid argument(s): Cannot find executable for nonesuch')
-      );
+    testWithoutContext('does not throw Exception and returns false if binary cannot run', () {
+      mockProcessManager.canRunSucceeds = false;
       expect(processUtils.exitsHappySync(<String>['nonesuch']), isFalse);
+      verifyNever(
+        mockProcessManager.runSync(any, environment: anyNamed('environment')),
+      );
+    });
+
+    testWithoutContext('does not catch ArgumentError', () async {
+      when(mockProcessManager.runSync(<String>['invalid'])).thenThrow(
+        ArgumentError('Bad input'),
+      );
+      expect(
+        () => processUtils.exitsHappySync(<String>['invalid']),
+        throwsArgumentError,
+      );
     });
   });
 
   group('exitsHappy', () {
-    ProcessManager mockProcessManager;
+    MockProcessManager mockProcessManager;
     ProcessUtils processUtils;
 
     setUp(() {
@@ -388,14 +399,14 @@
       );
     });
 
-    testWithoutContext(' succeeds on success', () async {
+    testWithoutContext('succeeds on success', () async {
       when(mockProcessManager.run(<String>['whoohoo'])).thenAnswer((_) {
         return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
       });
       expect(await processUtils.exitsHappy(<String>['whoohoo']), isTrue);
     });
 
-    testWithoutContext(' fails on failure', () async {
+    testWithoutContext('fails on failure', () async {
       when(mockProcessManager.run(<String>['boohoo'])).thenAnswer((_) {
         return Future<ProcessResult>.value(ProcessResult(0, 1, '', ''));
       });
@@ -409,11 +420,22 @@
       expect(await processUtils.exitsHappy(<String>['boohoo']), isFalse);
     });
 
-    testWithoutContext('catches ArgumentError and returns false', () async {
-      when(mockProcessManager.run(<String>['nonesuch'])).thenThrow(
-        ArgumentError('Invalid argument(s): Cannot find executable for nonesuch'),
-      );
+    testWithoutContext('does not throw Exception and returns false if binary cannot run', () async {
+      mockProcessManager.canRunSucceeds = false;
       expect(await processUtils.exitsHappy(<String>['nonesuch']), isFalse);
+      verifyNever(
+        mockProcessManager.runSync(any, environment: anyNamed('environment')),
+      );
+    });
+
+    testWithoutContext('does not catch ArgumentError', () async {
+      when(mockProcessManager.run(<String>['invalid'])).thenThrow(
+        ArgumentError('Bad input'),
+      );
+      expect(
+        () async => await processUtils.exitsHappy(<String>['invalid']),
+        throwsArgumentError,
+      );
     });
   });
 
diff --git a/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart
index f00d5aa..60e7650 100644
--- a/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart
@@ -30,6 +30,8 @@
 
     setUp(() async {
       mockProcessManager = MockProcessManager();
+      // Assume all binaries exist and are executable
+      when(mockProcessManager.canRun(any)).thenReturn(true);
       mockConfig = MockConfig();
       mockIosProject = MockIosProject();
       when(mockIosProject.buildSettings).thenAnswer((_) {
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 b1e78a4..fbba453 100644
--- a/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_test.dart
@@ -134,6 +134,11 @@
   }
 
   group('Evaluate installation', () {
+    setUp(() {
+      // Assume all binaries can run
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+    });
+
     testUsingContext('detects not installed, if pod exec does not exist', () async {
       pretendPodIsNotInstalled();
       expect(await cocoaPodsUnderTest.evaluateCocoaPodsInstallation, CocoaPodsStatus.notInstalled);
@@ -328,6 +333,8 @@
   group('Process pods', () {
     setUp(() {
       podsIsInHomeDir();
+      // Assume all binaries can run
+      when(mockProcessManager.canRun(any)).thenReturn(true);
     });
 
     testUsingContext('throwsToolExit if CocoaPods is not installed', () async {
@@ -674,6 +681,8 @@
     String cocoapodsRepoDir;
     Map<String, String> environment;
     setUp(() {
+      // Assume binaries exist and can run
+      when(mockProcessManager.canRun(any)).thenReturn(true);
       cocoapodsRepoDir = podsIsInCustomDir();
       environment = <String, String>{
         'FLUTTER_FRAMEWORK_DIR': 'engine/path',