[flutter_plugin_tools] Overhaul drive-examples (#4099)

Significantly restructures drive-examples:
- Migrates it to the new package-looping base command
- Enforces that only one platform is passed, since in practice multiple platforms never actually worked. (The logic is structured so that it will be easy to enable multi-platform if `flutter drive` gains multi-platform support.)
- Fixes the issue where `--ios` and `--android` were semi-broken, by doing explicit device targeting for them rather than relying on the default device being the right kind
- Extracts much of the logic to helpers so it's easier to understand the flow
- Removes support for a legacy integration test file structure that is no longer used
- Adds more test coverage; previously no failure cases were actually tested.

Fixes https://github.com/flutter/flutter/issues/85147
Part of https://github.com/flutter/flutter/issues/83413
diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart
index c8280f4..ee1445f 100644
--- a/script/tool/lib/src/build_examples_command.dart
+++ b/script/tool/lib/src/build_examples_command.dart
@@ -6,7 +6,6 @@
 
 import 'package:file/file.dart';
 import 'package:path/path.dart' as p;
-import 'package:platform/platform.dart';
 
 import 'common/core.dart';
 import 'common/package_looping_command.dart';
@@ -151,8 +150,6 @@
     String flutterBuildType, {
     List<String> extraBuildFlags = const <String>[],
   }) async {
-    final String flutterCommand =
-        const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter';
     final String enableExperiment = getStringArg(kEnableExperiment);
 
     final int exitCode = await processRunner.runAndStream(
diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart
index 9a96ab1..43d0d0b 100644
--- a/script/tool/lib/src/common/plugin_command.dart
+++ b/script/tool/lib/src/common/plugin_command.dart
@@ -8,6 +8,7 @@
 import 'package:file/file.dart';
 import 'package:git/git.dart';
 import 'package:path/path.dart' as p;
+import 'package:platform/platform.dart';
 
 import 'core.dart';
 import 'git_version_finder.dart';
@@ -85,6 +86,10 @@
   int? _shardIndex;
   int? _shardCount;
 
+  /// The command to use when running `flutter`.
+  String get flutterCommand =>
+      const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter';
+
   /// The shard of the overall command execution that this instance should run.
   int get shardIndex {
     if (_shardIndex == null) {
diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart
index 8a8cd67..a4aa7c1 100644
--- a/script/tool/lib/src/drive_examples_command.dart
+++ b/script/tool/lib/src/drive_examples_command.dart
@@ -2,17 +2,22 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:convert';
+import 'dart:io';
+
 import 'package:file/file.dart';
 import 'package:path/path.dart' as p;
-import 'package:platform/platform.dart';
 
 import 'common/core.dart';
-import 'common/plugin_command.dart';
+import 'common/package_looping_command.dart';
 import 'common/plugin_utils.dart';
 import 'common/process_runner.dart';
 
+const int _exitNoPlatformFlags = 2;
+const int _exitNoAvailableDevice = 3;
+
 /// A command to run the example applications for packages via Flutter driver.
-class DriveExamplesCommand extends PluginCommand {
+class DriveExamplesCommand extends PackageLoopingCommand {
   /// Creates an instance of the drive command.
   DriveExamplesCommand(
     Directory packagesDir, {
@@ -43,213 +48,259 @@
 
   @override
   final String description = 'Runs driver tests for plugin example apps.\n\n'
-      'For each *_test.dart in test_driver/ it drives an application with a '
-      'corresponding name in the test/ or test_driver/ directories.\n\n'
-      'For example, test_driver/app_test.dart would match test/app.dart.\n\n'
-      'This command requires "flutter" to be in your path.\n\n'
-      'If a file with a corresponding name cannot be found, this driver file'
-      'will be used to drive the tests that match '
-      'integration_test/*_test.dart.';
+      'For each *_test.dart in test_driver/ it drives an application with '
+      'either the corresponding test in test_driver (for example, '
+      'test_driver/app_test.dart would match test_driver/app.dart), or the '
+      '*_test.dart files in integration_test/.\n\n'
+      'This command requires "flutter" to be in your path.';
+
+  Map<String, List<String>> _targetDeviceFlags = const <String, List<String>>{};
 
   @override
-  Future<void> run() async {
-    final List<String> failingTests = <String>[];
-    final List<String> pluginsWithoutTests = <String>[];
-    final bool isLinux = getBoolArg(kPlatformLinux);
-    final bool isMacos = getBoolArg(kPlatformMacos);
-    final bool isWeb = getBoolArg(kPlatformWeb);
-    final bool isWindows = getBoolArg(kPlatformWindows);
-    await for (final Directory plugin in getPlugins()) {
-      final String pluginName = plugin.basename;
-      if (pluginName.endsWith('_platform_interface') &&
-          !plugin.childDirectory('example').existsSync()) {
-        // Platform interface packages generally aren't intended to have
-        // examples, and don't need integration tests, so silently skip them
-        // unless for some reason there is an example directory.
-        continue;
-      }
-      print('\n==========\nChecking $pluginName...');
-      if (!(await _pluginSupportedOnCurrentPlatform(plugin))) {
-        print('Not supported for the target platform; skipping.');
-        continue;
-      }
-      int examplesFound = 0;
-      bool testsRan = false;
-      final String flutterCommand =
-          const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter';
-      for (final Directory example in getExamplesForPlugin(plugin)) {
-        ++examplesFound;
-        final String packageName =
-            p.relative(example.path, from: packagesDir.path);
-        final Directory driverTests = example.childDirectory('test_driver');
-        if (!driverTests.existsSync()) {
-          print('No driver tests found for $packageName');
-          continue;
-        }
-        // Look for driver tests ending in _test.dart in test_driver/
-        await for (final FileSystemEntity test in driverTests.list()) {
-          final String driverTestName =
-              p.relative(test.path, from: driverTests.path);
-          if (!driverTestName.endsWith('_test.dart')) {
-            continue;
-          }
-          // Try to find a matching app to drive without the _test.dart
-          final String deviceTestName = driverTestName.replaceAll(
-            RegExp(r'_test.dart$'),
-            '.dart',
-          );
-          String deviceTestPath = p.join('test', deviceTestName);
-          if (!example.fileSystem
-              .file(p.join(example.path, deviceTestPath))
-              .existsSync()) {
-            // If the app isn't in test/ folder, look in test_driver/ instead.
-            deviceTestPath = p.join('test_driver', deviceTestName);
-          }
-
-          final List<String> targetPaths = <String>[];
-          if (example.fileSystem
-              .file(p.join(example.path, deviceTestPath))
-              .existsSync()) {
-            targetPaths.add(deviceTestPath);
-          } else {
-            final Directory integrationTests =
-                example.childDirectory('integration_test');
-
-            if (await integrationTests.exists()) {
-              await for (final FileSystemEntity integrationTest
-                  in integrationTests.list()) {
-                if (!integrationTest.basename.endsWith('_test.dart')) {
-                  continue;
-                }
-                targetPaths
-                    .add(p.relative(integrationTest.path, from: example.path));
-              }
-            }
-
-            if (targetPaths.isEmpty) {
-              print('''
-Unable to infer a target application for $driverTestName to drive.
-Tried searching for the following:
-1. test/$deviceTestName
-2. test_driver/$deviceTestName
-3. test_driver/*_test.dart
-''');
-              failingTests.add(p.relative(test.path, from: example.path));
-              continue;
-            }
-          }
-
-          final List<String> driveArgs = <String>['drive'];
-
-          final String enableExperiment = getStringArg(kEnableExperiment);
-          if (enableExperiment.isNotEmpty) {
-            driveArgs.add('--enable-experiment=$enableExperiment');
-          }
-
-          if (isLinux && isLinuxPlugin(plugin)) {
-            driveArgs.addAll(<String>[
-              '-d',
-              'linux',
-            ]);
-          }
-          if (isMacos && isMacOsPlugin(plugin)) {
-            driveArgs.addAll(<String>[
-              '-d',
-              'macos',
-            ]);
-          }
-          if (isWeb && isWebPlugin(plugin)) {
-            driveArgs.addAll(<String>[
-              '-d',
-              'web-server',
-              '--web-port=7357',
-              '--browser-name=chrome',
-            ]);
-          }
-          if (isWindows && isWindowsPlugin(plugin)) {
-            driveArgs.addAll(<String>[
-              '-d',
-              'windows',
-            ]);
-          }
-
-          for (final String targetPath in targetPaths) {
-            testsRan = true;
-            final int exitCode = await processRunner.runAndStream(
-                flutterCommand,
-                <String>[
-                  ...driveArgs,
-                  '--driver',
-                  p.join('test_driver', driverTestName),
-                  '--target',
-                  targetPath,
-                ],
-                workingDir: example,
-                exitOnError: true);
-            if (exitCode != 0) {
-              failingTests.add(p.join(packageName, deviceTestPath));
-            }
-          }
-        }
-      }
-      if (!testsRan) {
-        pluginsWithoutTests.add(pluginName);
-        print(
-            'No driver tests run for $pluginName ($examplesFound examples found)');
-      }
-    }
-    print('\n\n');
-
-    if (failingTests.isNotEmpty) {
-      print('The following driver tests are failing (see above for details):');
-      for (final String test in failingTests) {
-        print(' * $test');
-      }
-      throw ToolExit(1);
+  Future<void> initializeRun() async {
+    final List<String> platformSwitches = <String>[
+      kPlatformAndroid,
+      kPlatformIos,
+      kPlatformLinux,
+      kPlatformMacos,
+      kPlatformWeb,
+      kPlatformWindows,
+    ];
+    final int platformCount = platformSwitches
+        .where((String platform) => getBoolArg(platform))
+        .length;
+    // The flutter tool currently doesn't accept multiple device arguments:
+    // https://github.com/flutter/flutter/issues/35733
+    // If that is implemented, this check can be relaxed.
+    if (platformCount != 1) {
+      printError(
+          'Exactly one of ${platformSwitches.map((String platform) => '--$platform').join(', ')} '
+          'must be specified.');
+      throw ToolExit(_exitNoPlatformFlags);
     }
 
-    if (pluginsWithoutTests.isNotEmpty) {
-      print('The following plugins did not run any integration tests:');
-      for (final String plugin in pluginsWithoutTests) {
-        print(' * $plugin');
+    String? androidDevice;
+    if (getBoolArg(kPlatformAndroid)) {
+      final List<String> devices = await _getDevicesForPlatform('android');
+      if (devices.isEmpty) {
+        printError('No Android devices available');
+        throw ToolExit(_exitNoAvailableDevice);
       }
-      print('If this is intentional, they must be explicitly excluded.');
-      throw ToolExit(1);
+      androidDevice = devices.first;
     }
 
-    print('All driver tests successful!');
+    String? iosDevice;
+    if (getBoolArg(kPlatformIos)) {
+      final List<String> devices = await _getDevicesForPlatform('ios');
+      if (devices.isEmpty) {
+        printError('No iOS devices available');
+        throw ToolExit(_exitNoAvailableDevice);
+      }
+      iosDevice = devices.first;
+    }
+
+    _targetDeviceFlags = <String, List<String>>{
+      if (getBoolArg(kPlatformAndroid))
+        kPlatformAndroid: <String>['-d', androidDevice!],
+      if (getBoolArg(kPlatformIos)) kPlatformIos: <String>['-d', iosDevice!],
+      if (getBoolArg(kPlatformLinux)) kPlatformLinux: <String>['-d', 'linux'],
+      if (getBoolArg(kPlatformMacos)) kPlatformMacos: <String>['-d', 'macos'],
+      if (getBoolArg(kPlatformWeb))
+        kPlatformWeb: <String>[
+          '-d',
+          'web-server',
+          '--web-port=7357',
+          '--browser-name=chrome'
+        ],
+      if (getBoolArg(kPlatformWindows))
+        kPlatformWindows: <String>['-d', 'windows'],
+    };
   }
 
-  Future<bool> _pluginSupportedOnCurrentPlatform(
-      FileSystemEntity plugin) async {
-    final bool isAndroid = getBoolArg(kPlatformAndroid);
-    final bool isIOS = getBoolArg(kPlatformIos);
-    final bool isLinux = getBoolArg(kPlatformLinux);
-    final bool isMacos = getBoolArg(kPlatformMacos);
-    final bool isWeb = getBoolArg(kPlatformWeb);
-    final bool isWindows = getBoolArg(kPlatformWindows);
-    if (isAndroid) {
-      return isAndroidPlugin(plugin);
+  @override
+  Future<List<String>> runForPackage(Directory package) async {
+    if (package.basename.endsWith('_platform_interface') &&
+        !package.childDirectory('example').existsSync()) {
+      // Platform interface packages generally aren't intended to have
+      // examples, and don't need integration tests, so skip rather than fail.
+      printSkip(
+          'Platform interfaces are not expected to have integratino tests.');
+      return PackageLoopingCommand.success;
     }
-    if (isIOS) {
-      return isIosPlugin(plugin);
+
+    final List<String> deviceFlags = <String>[];
+    for (final MapEntry<String, List<String>> entry
+        in _targetDeviceFlags.entries) {
+      if (pluginSupportsPlatform(entry.key, package)) {
+        deviceFlags.addAll(entry.value);
+      } else {
+        print('Skipping unsupported platform ${entry.key}...');
+      }
     }
-    if (isLinux) {
-      return isLinuxPlugin(plugin);
+    // If there is no supported target platform, skip the plugin.
+    if (deviceFlags.isEmpty) {
+      printSkip(
+          '${getPackageDescription(package)} does not support any requested platform.');
+      return PackageLoopingCommand.success;
     }
-    if (isMacos) {
-      return isMacOsPlugin(plugin);
+
+    int examplesFound = 0;
+    bool testsRan = false;
+    final List<String> errors = <String>[];
+    for (final Directory example in getExamplesForPlugin(package)) {
+      ++examplesFound;
+      final String exampleName =
+          p.relative(example.path, from: packagesDir.path);
+
+      final List<File> drivers = await _getDrivers(example);
+      if (drivers.isEmpty) {
+        print('No driver tests found for $exampleName');
+        continue;
+      }
+
+      for (final File driver in drivers) {
+        final List<File> testTargets = <File>[];
+
+        // Try to find a matching app to drive without the _test.dart
+        // TODO(stuartmorgan): Migrate all remaining uses of this legacy
+        // approach (currently only video_player) and remove support for it:
+        // https://github.com/flutter/flutter/issues/85224.
+        final File? legacyTestFile = _getLegacyTestFileForTestDriver(driver);
+        if (legacyTestFile != null) {
+          testTargets.add(legacyTestFile);
+        } else {
+          (await _getIntegrationTests(example)).forEach(testTargets.add);
+        }
+
+        if (testTargets.isEmpty) {
+          final String driverRelativePath =
+              p.relative(driver.path, from: package.path);
+          printError(
+              'Found $driverRelativePath, but no integration_test/*_test.dart files.');
+          errors.add(
+              'No test files for ${p.relative(driver.path, from: package.path)}');
+          continue;
+        }
+
+        testsRan = true;
+        final List<File> failingTargets = await _driveTests(
+            example, driver, testTargets,
+            deviceFlags: deviceFlags);
+        for (final File failingTarget in failingTargets) {
+          errors.add(p.relative(failingTarget.path, from: package.path));
+        }
+      }
     }
-    if (isWeb) {
-      return isWebPlugin(plugin);
+    if (!testsRan) {
+      printError('No driver tests were run ($examplesFound example(s) found).');
+      errors.add('No tests ran (use --exclude if this is intentional).');
     }
-    if (isWindows) {
-      return isWindowsPlugin(plugin);
+    return errors;
+  }
+
+  Future<List<String>> _getDevicesForPlatform(String platform) async {
+    final List<String> deviceIds = <String>[];
+
+    final ProcessResult result = await processRunner.run(
+        flutterCommand, <String>['devices', '--machine'],
+        stdoutEncoding: utf8, exitOnError: true);
+    if (result.exitCode != 0) {
+      return deviceIds;
     }
-    // When we are here, no flags are specified. Only return true if the plugin
-    // supports Android for legacy command support.
-    // TODO(cyanglaz): Make Android flag also required like other platforms
-    // (breaking change). https://github.com/flutter/flutter/issues/58285
-    return isAndroidPlugin(plugin);
+
+    final List<Map<String, dynamic>> devices =
+        (jsonDecode(result.stdout as String) as List<dynamic>)
+            .cast<Map<String, dynamic>>();
+    for (final Map<String, dynamic> deviceInfo in devices) {
+      final String targetPlatform =
+          (deviceInfo['targetPlatform'] as String?) ?? '';
+      if (targetPlatform.startsWith(platform)) {
+        final String? deviceId = deviceInfo['id'] as String?;
+        if (deviceId != null) {
+          deviceIds.add(deviceId);
+        }
+      }
+    }
+    return deviceIds;
+  }
+
+  Future<List<File>> _getDrivers(Directory example) async {
+    final List<File> drivers = <File>[];
+
+    final Directory driverDir = example.childDirectory('test_driver');
+    if (driverDir.existsSync()) {
+      await for (final FileSystemEntity driver in driverDir.list()) {
+        if (driver is File && driver.basename.endsWith('_test.dart')) {
+          drivers.add(driver);
+        }
+      }
+    }
+    return drivers;
+  }
+
+  File? _getLegacyTestFileForTestDriver(File testDriver) {
+    final String testName = testDriver.basename.replaceAll(
+      RegExp(r'_test.dart$'),
+      '.dart',
+    );
+    final File testFile = testDriver.parent.childFile(testName);
+
+    return testFile.existsSync() ? testFile : null;
+  }
+
+  Future<List<File>> _getIntegrationTests(Directory example) async {
+    final List<File> tests = <File>[];
+    final Directory integrationTestDir =
+        example.childDirectory('integration_test');
+
+    if (integrationTestDir.existsSync()) {
+      await for (final FileSystemEntity file in integrationTestDir.list()) {
+        if (file is File && file.basename.endsWith('_test.dart')) {
+          tests.add(file);
+        }
+      }
+    }
+    return tests;
+  }
+
+  /// For each file in [targets], uses
+  /// `flutter drive --driver [driver] --target <target>`
+  /// to drive [example], returning a list of any failing test targets.
+  ///
+  /// [deviceFlags] should contain the flags to run the test on a specific
+  /// target device (plus any supporting device-specific flags). E.g.:
+  ///   - `['-d', 'macos']` for driving for macOS.
+  ///   - `['-d', 'web-server', '--web-port=<port>', '--browser-name=<browser>]`
+  ///     for web
+  Future<List<File>> _driveTests(
+    Directory example,
+    File driver,
+    List<File> targets, {
+    required List<String> deviceFlags,
+  }) async {
+    final List<File> failures = <File>[];
+
+    final String enableExperiment = getStringArg(kEnableExperiment);
+
+    for (final File target in targets) {
+      final int exitCode = await processRunner.runAndStream(
+          flutterCommand,
+          <String>[
+            'drive',
+            ...deviceFlags,
+            if (enableExperiment.isNotEmpty)
+              '--enable-experiment=$enableExperiment',
+            '--driver',
+            p.relative(driver.path, from: example.path),
+            '--target',
+            p.relative(target.path, from: example.path),
+          ],
+          workingDir: example,
+          exitOnError: true);
+      if (exitCode != 0) {
+        failures.add(target);
+      }
+    }
+    return failures;
   }
 }
diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart
index 3175f71..e441a0f 100644
--- a/script/tool/test/drive_examples_command_test.dart
+++ b/script/tool/test/drive_examples_command_test.dart
@@ -12,8 +12,12 @@
 import 'package:platform/platform.dart';
 import 'package:test/test.dart';
 
+import 'mocks.dart';
 import 'util.dart';
 
+const String _fakeIosDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda';
+const String _fakeAndroidDevice = 'emulator-1234';
+
 void main() {
   group('test drive_example_command', () {
     late FileSystem fileSystem;
@@ -35,52 +39,92 @@
       runner.addCommand(command);
     });
 
-    test('driving under folder "test"', () async {
-      final Directory pluginDirectory = createFakePlugin(
-        'plugin',
-        packagesDir,
-        extraFiles: <String>[
-          'example/test_driver/plugin_test.dart',
-          'example/test/plugin.dart',
-        ],
-        platformSupport: <String, PlatformSupport>{
-          kPlatformAndroid: PlatformSupport.inline,
-          kPlatformIos: PlatformSupport.inline,
-        },
-      );
+    void setMockFlutterDevicesOutput({
+      bool hasIosDevice = true,
+      bool hasAndroidDevice = true,
+    }) {
+      final List<String> devices = <String>[
+        if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}',
+        if (hasAndroidDevice)
+          '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}',
+      ];
+      final String output = '''[${devices.join(',')}]''';
 
-      final Directory pluginExampleDirectory =
-          pluginDirectory.childDirectory('example');
+      final MockProcess mockDevicesProcess = MockProcess();
+      mockDevicesProcess.exitCodeCompleter.complete(0);
+      mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures
+      processRunner.processToReturn = mockDevicesProcess;
+      processRunner.resultStdout = output;
+    }
 
-      final List<String> output = await runCapturingPrint(runner, <String>[
-        'drive-examples',
-      ]);
+    test('fails if no platforms are provided', () async {
+      setMockFlutterDevicesOutput();
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples'], errorHandler: (Error e) {
+        commandError = e;
+      });
 
+      expect(commandError, isA<ToolExit>());
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Exactly one of'),
         ]),
       );
+    });
 
-      final String deviceTestPath = p.join('test', 'plugin.dart');
-      final String driverTestPath = p.join('test_driver', 'plugin_test.dart');
+    test('fails if multiple platforms are provided', () async {
+      setMockFlutterDevicesOutput();
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--ios', '--macos'],
+          errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
       expect(
-          processRunner.recordedCalls,
-          orderedEquals(<ProcessCall>[
-            ProcessCall(
-                flutterCommand,
-                <String>[
-                  'drive',
-                  '--driver',
-                  driverTestPath,
-                  '--target',
-                  deviceTestPath
-                ],
-                pluginExampleDirectory.path),
-          ]));
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Exactly one of'),
+        ]),
+      );
+    });
+
+    test('fails for iOS if no iOS devices are present', () async {
+      setMockFlutterDevicesOutput(hasIosDevice: false);
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--ios'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('No iOS devices'),
+        ]),
+      );
+    });
+
+    test('fails if Android if no Android devices are present', () async {
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--android'],
+          errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('No Android devices'),
+        ]),
+      );
     });
 
     test('driving under folder "test_driver"', () async {
@@ -100,16 +144,15 @@
       final Directory pluginExampleDirectory =
           pluginDirectory.childDirectory('example');
 
-      final List<String> output = await runCapturingPrint(runner, <String>[
-        'drive-examples',
-      ]);
+      setMockFlutterDevicesOutput();
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['drive-examples', '--ios']);
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -119,9 +162,13 @@
           processRunner.recordedCalls,
           orderedEquals(<ProcessCall>[
             ProcessCall(
+                flutterCommand, const <String>['devices', '--machine'], null),
+            ProcessCall(
                 flutterCommand,
                 <String>[
                   'drive',
+                  '-d',
+                  _fakeIosDevice,
                   '--driver',
                   driverTestPath,
                   '--target',
@@ -133,6 +180,7 @@
 
     test('driving under folder "test_driver" when test files are missing"',
         () async {
+      setMockFlutterDevicesOutput();
       createFakePlugin(
         'plugin',
         packagesDir,
@@ -145,13 +193,27 @@
         },
       );
 
-      await expectLater(
-          () => runCapturingPrint(runner, <String>['drive-examples']),
-          throwsA(const TypeMatcher<ToolExit>()));
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--android'],
+          errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No driver tests were run (1 example(s) found).'),
+          contains('No test files for example/test_driver/plugin_test.dart'),
+        ]),
+      );
     });
 
     test('a plugin without any integration test files is reported as an error',
         () async {
+      setMockFlutterDevicesOutput();
       createFakePlugin(
         'plugin',
         packagesDir,
@@ -164,9 +226,22 @@
         },
       );
 
-      await expectLater(
-          () => runCapturingPrint(runner, <String>['drive-examples']),
-          throwsA(const TypeMatcher<ToolExit>()));
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--android'],
+          errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No driver tests were run (1 example(s) found).'),
+          contains('No tests ran'),
+        ]),
+      );
     });
 
     test(
@@ -190,16 +265,15 @@
       final Directory pluginExampleDirectory =
           pluginDirectory.childDirectory('example');
 
-      final List<String> output = await runCapturingPrint(runner, <String>[
-        'drive-examples',
-      ]);
+      setMockFlutterDevicesOutput();
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['drive-examples', '--ios']);
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -209,9 +283,13 @@
           processRunner.recordedCalls,
           orderedEquals(<ProcessCall>[
             ProcessCall(
+                flutterCommand, const <String>['devices', '--machine'], null),
+            ProcessCall(
                 flutterCommand,
                 <String>[
                   'drive',
+                  '-d',
+                  _fakeIosDevice,
                   '--driver',
                   driverTestPath,
                   '--target',
@@ -222,6 +300,8 @@
                 flutterCommand,
                 <String>[
                   'drive',
+                  '-d',
+                  _fakeIosDevice,
                   '--driver',
                   driverTestPath,
                   '--target',
@@ -244,11 +324,10 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          'Not supported for the target platform; skipping.',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Skipping unsupported platform linux...'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -280,10 +359,9 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -320,11 +398,10 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          'Not supported for the target platform; skipping.',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Skipping unsupported platform macos...'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -332,6 +409,7 @@
       // implementation is a no-op.
       expect(processRunner.recordedCalls, <ProcessCall>[]);
     });
+
     test('driving on a macOS plugin', () async {
       final Directory pluginDirectory = createFakePlugin(
         'plugin',
@@ -356,10 +434,9 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -396,11 +473,9 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          'Not supported for the target platform; skipping.',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -432,10 +507,9 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -474,11 +548,10 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          'Not supported for the target platform; skipping.',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Skipping unsupported platform windows...'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -510,10 +583,9 @@
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
         ]),
       );
 
@@ -537,7 +609,59 @@
           ]));
     });
 
-    test('driving when plugin does not support mobile is no-op', () async {
+    test('driving on an Android plugin', () async {
+      final Directory pluginDirectory = createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/test_driver/plugin_test.dart',
+          'example/test_driver/plugin.dart',
+        ],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformAndroid: PlatformSupport.inline,
+        },
+      );
+
+      final Directory pluginExampleDirectory =
+          pluginDirectory.childDirectory('example');
+
+      setMockFlutterDevicesOutput();
+      final List<String> output = await runCapturingPrint(runner, <String>[
+        'drive-examples',
+        '--android',
+      ]);
+
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No issues found!'),
+        ]),
+      );
+
+      final String deviceTestPath = p.join('test_driver', 'plugin.dart');
+      final String driverTestPath = p.join('test_driver', 'plugin_test.dart');
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
+                flutterCommand, const <String>['devices', '--machine'], null),
+            ProcessCall(
+                flutterCommand,
+                <String>[
+                  'drive',
+                  '-d',
+                  _fakeAndroidDevice,
+                  '--driver',
+                  driverTestPath,
+                  '--target',
+                  deviceTestPath
+                ],
+                pluginExampleDirectory.path),
+          ]));
+    });
+
+    test('driving when plugin does not support Android is no-op', () async {
       createFakePlugin(
         'plugin',
         packagesDir,
@@ -550,43 +674,78 @@
         },
       );
 
-      final List<String> output = await runCapturingPrint(runner, <String>[
-        'drive-examples',
-      ]);
+      setMockFlutterDevicesOutput();
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--android']);
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n==========\nChecking plugin...',
-          'Not supported for the target platform; skipping.',
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Skipping unsupported platform android...'),
+          contains('No issues found!'),
         ]),
       );
 
-      // Output should be empty since running drive-examples --macos with no macos
-      // implementation is a no-op.
-      expect(processRunner.recordedCalls, <ProcessCall>[]);
+      // Output should be empty other than the device query.
+      expect(processRunner.recordedCalls, <ProcessCall>[
+        ProcessCall(
+            flutterCommand, const <String>['devices', '--machine'], null),
+      ]);
+    });
+
+    test('driving when plugin does not support iOS is no-op', () async {
+      createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/test_driver/plugin_test.dart',
+          'example/test_driver/plugin.dart',
+        ],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformMacos: PlatformSupport.inline,
+        },
+      );
+
+      setMockFlutterDevicesOutput();
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['drive-examples', '--ios']);
+
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Skipping unsupported platform ios...'),
+          contains('No issues found!'),
+        ]),
+      );
+
+      // Output should be empty other than the device query.
+      expect(processRunner.recordedCalls, <ProcessCall>[
+        ProcessCall(
+            flutterCommand, const <String>['devices', '--machine'], null),
+      ]);
     });
 
     test('platform interface plugins are silently skipped', () async {
       createFakePlugin('aplugin_platform_interface', packagesDir,
           examples: <String>[]);
 
-      final List<String> output = await runCapturingPrint(runner, <String>[
-        'drive-examples',
-      ]);
+      setMockFlutterDevicesOutput();
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--macos']);
 
       expect(
         output,
-        orderedEquals(<String>[
-          '\n\n',
-          'All driver tests successful!',
+        containsAllInOrder(<Matcher>[
+          contains('Running for aplugin_platform_interface'),
+          contains(
+              'SKIPPING: Platform interfaces are not expected to have integratino tests.'),
+          contains('No issues found!'),
         ]),
       );
 
-      // Output should be empty since running drive-examples --macos with no macos
-      // implementation is a no-op.
+      // Output should be empty since it's skipped.
       expect(processRunner.recordedCalls, <ProcessCall>[]);
     });
 
@@ -596,7 +755,7 @@
         packagesDir,
         extraFiles: <String>[
           'example/test_driver/plugin_test.dart',
-          'example/test/plugin.dart',
+          'example/test_driver/plugin.dart',
         ],
         platformSupport: <String, PlatformSupport>{
           kPlatformAndroid: PlatformSupport.inline,
@@ -607,25 +766,199 @@
       final Directory pluginExampleDirectory =
           pluginDirectory.childDirectory('example');
 
+      setMockFlutterDevicesOutput();
       await runCapturingPrint(runner, <String>[
         'drive-examples',
+        '--ios',
         '--enable-experiment=exp1',
       ]);
 
-      final String deviceTestPath = p.join('test', 'plugin.dart');
+      final String deviceTestPath = p.join('test_driver', 'plugin.dart');
       final String driverTestPath = p.join('test_driver', 'plugin_test.dart');
       expect(
           processRunner.recordedCalls,
           orderedEquals(<ProcessCall>[
             ProcessCall(
+                flutterCommand, const <String>['devices', '--machine'], null),
+            ProcessCall(
+                flutterCommand,
+                <String>[
+                  'drive',
+                  '-d',
+                  _fakeIosDevice,
+                  '--enable-experiment=exp1',
+                  '--driver',
+                  driverTestPath,
+                  '--target',
+                  deviceTestPath
+                ],
+                pluginExampleDirectory.path),
+          ]));
+    });
+
+    test('fails when no example is present', () async {
+      createFakePlugin(
+        'plugin',
+        packagesDir,
+        examples: <String>[],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformWeb: PlatformSupport.inline,
+        },
+      );
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--web'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No driver tests were run (0 example(s) found).'),
+          contains('The following packages had errors:'),
+          contains('  plugin:\n'
+              '    No tests ran (use --exclude if this is intentional)'),
+        ]),
+      );
+    });
+
+    test('fails when no driver is present', () async {
+      createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/integration_test/bar_test.dart',
+          'example/integration_test/foo_test.dart',
+        ],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformWeb: PlatformSupport.inline,
+        },
+      );
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--web'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('No driver tests found for plugin/example'),
+          contains('No driver tests were run (1 example(s) found).'),
+          contains('The following packages had errors:'),
+          contains('  plugin:\n'
+              '    No tests ran (use --exclude if this is intentional)'),
+        ]),
+      );
+    });
+
+    test('fails when no integration tests are present', () async {
+      createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/test_driver/integration_test.dart',
+        ],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformWeb: PlatformSupport.inline,
+        },
+      );
+
+      Error? commandError;
+      final List<String> output = await runCapturingPrint(
+          runner, <String>['drive-examples', '--web'], errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('Found example/test_driver/integration_test.dart, but no '
+              'integration_test/*_test.dart files.'),
+          contains('No driver tests were run (1 example(s) found).'),
+          contains('The following packages had errors:'),
+          contains('  plugin:\n'
+              '    No test files for example/test_driver/integration_test.dart\n'
+              '    No tests ran (use --exclude if this is intentional)'),
+        ]),
+      );
+    });
+
+    test('reports test failures', () async {
+      final Directory pluginDirectory = createFakePlugin(
+        'plugin',
+        packagesDir,
+        extraFiles: <String>[
+          'example/test_driver/integration_test.dart',
+          'example/integration_test/bar_test.dart',
+          'example/integration_test/foo_test.dart',
+        ],
+        platformSupport: <String, PlatformSupport>{
+          kPlatformMacos: PlatformSupport.inline,
+        },
+      );
+
+      // Simulate failure from `flutter drive`.
+      final MockProcess mockDriveProcess = MockProcess();
+      mockDriveProcess.exitCodeCompleter.complete(1);
+      processRunner.processToReturn = mockDriveProcess;
+
+      Error? commandError;
+      final List<String> output =
+          await runCapturingPrint(runner, <String>['drive-examples', '--macos'],
+              errorHandler: (Error e) {
+        commandError = e;
+      });
+
+      expect(commandError, isA<ToolExit>());
+      expect(
+        output,
+        containsAllInOrder(<Matcher>[
+          contains('Running for plugin'),
+          contains('The following packages had errors:'),
+          contains('  plugin:\n'
+              '    example/integration_test/bar_test.dart\n'
+              '    example/integration_test/foo_test.dart'),
+        ]),
+      );
+
+      final Directory pluginExampleDirectory =
+          pluginDirectory.childDirectory('example');
+      final String driverTestPath =
+          p.join('test_driver', 'integration_test.dart');
+      expect(
+          processRunner.recordedCalls,
+          orderedEquals(<ProcessCall>[
+            ProcessCall(
                 flutterCommand,
                 <String>[
                   'drive',
-                  '--enable-experiment=exp1',
+                  '-d',
+                  'macos',
                   '--driver',
                   driverTestPath,
                   '--target',
-                  deviceTestPath
+                  p.join('integration_test', 'bar_test.dart'),
+                ],
+                pluginExampleDirectory.path),
+            ProcessCall(
+                flutterCommand,
+                <String>[
+                  'drive',
+                  '-d',
+                  'macos',
+                  '--driver',
+                  driverTestPath,
+                  '--target',
+                  p.join('integration_test', 'foo_test.dart'),
                 ],
                 pluginExampleDirectory.path),
           ]));