diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index c8280f4e867c..ee1445fa8b7f 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -6,7 +6,6 @@ import 'dart:async'; 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 @@ class BuildExamplesCommand extends PackageLoopingCommand { String flutterBuildType, { List extraBuildFlags = const [], }) 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 9a96ab13443d..43d0d0b822c7 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:args/command_runner.dart'; 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 @@ abstract class PluginCommand extends Command { 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 8a8cd6726d02..a4aa7c12913d 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 @@ class DriveExamplesCommand extends PluginCommand { @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> _targetDeviceFlags = const >{}; @override - Future run() async { - final List failingTests = []; - final List pluginsWithoutTests = []; - 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; + Future initializeRun() async { + final List platformSwitches = [ + 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); + } + + String? androidDevice; + if (getBoolArg(kPlatformAndroid)) { + final List devices = await _getDevicesForPlatform('android'); + if (devices.isEmpty) { + printError('No Android devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + androidDevice = devices.first; + } + + String? iosDevice; + if (getBoolArg(kPlatformIos)) { + final List devices = await _getDevicesForPlatform('ios'); + if (devices.isEmpty) { + printError('No iOS devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + iosDevice = devices.first; + } + + _targetDeviceFlags = >{ + if (getBoolArg(kPlatformAndroid)) + kPlatformAndroid: ['-d', androidDevice!], + if (getBoolArg(kPlatformIos)) kPlatformIos: ['-d', iosDevice!], + if (getBoolArg(kPlatformLinux)) kPlatformLinux: ['-d', 'linux'], + if (getBoolArg(kPlatformMacos)) kPlatformMacos: ['-d', 'macos'], + if (getBoolArg(kPlatformWeb)) + kPlatformWeb: [ + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome' + ], + if (getBoolArg(kPlatformWindows)) + kPlatformWindows: ['-d', 'windows'], + }; + } + + @override + Future> 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; + } + + final List deviceFlags = []; + for (final MapEntry> entry + in _targetDeviceFlags.entries) { + if (pluginSupportsPlatform(entry.key, package)) { + deviceFlags.addAll(entry.value); + } else { + print('Skipping unsupported platform ${entry.key}...'); } - print('\n==========\nChecking $pluginName...'); - if (!(await _pluginSupportedOnCurrentPlatform(plugin))) { - print('Not supported for the target platform; skipping.'); + } + // 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; + } + + int examplesFound = 0; + bool testsRan = false; + final List errors = []; + for (final Directory example in getExamplesForPlugin(package)) { + ++examplesFound; + final String exampleName = + p.relative(example.path, from: packagesDir.path); + + final List drivers = await _getDrivers(example); + if (drivers.isEmpty) { + print('No driver tests found for $exampleName'); 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'); + + for (final File driver in drivers) { + final List testTargets = []; + + // 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; } - // 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 targetPaths = []; - 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 driveArgs = ['drive']; - - final String enableExperiment = getStringArg(kEnableExperiment); - if (enableExperiment.isNotEmpty) { - driveArgs.add('--enable-experiment=$enableExperiment'); - } - - if (isLinux && isLinuxPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'linux', - ]); - } - if (isMacos && isMacOsPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'macos', - ]); - } - if (isWeb && isWebPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - ]); - } - if (isWindows && isWindowsPlugin(plugin)) { - driveArgs.addAll([ - '-d', - 'windows', - ]); - } - - for (final String targetPath in targetPaths) { - testsRan = true; - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - ...driveArgs, - '--driver', - p.join('test_driver', driverTestName), - '--target', - targetPath, - ], - workingDir: example, - exitOnError: true); - if (exitCode != 0) { - failingTests.add(p.join(packageName, deviceTestPath)); - } - } + + testsRan = true; + final List failingTargets = await _driveTests( + example, driver, testTargets, + deviceFlags: deviceFlags); + for (final File failingTarget in failingTargets) { + errors.add(p.relative(failingTarget.path, from: package.path)); } } - if (!testsRan) { - pluginsWithoutTests.add(pluginName); - print( - 'No driver tests run for $pluginName ($examplesFound examples found)'); - } } - print('\n\n'); + if (!testsRan) { + printError('No driver tests were run ($examplesFound example(s) found).'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + return errors; + } + + Future> _getDevicesForPlatform(String platform) async { + final List deviceIds = []; - if (failingTests.isNotEmpty) { - print('The following driver tests are failing (see above for details):'); - for (final String test in failingTests) { - print(' * $test'); + final ProcessResult result = await processRunner.run( + flutterCommand, ['devices', '--machine'], + stdoutEncoding: utf8, exitOnError: true); + if (result.exitCode != 0) { + return deviceIds; + } + + final List> devices = + (jsonDecode(result.stdout as String) as List) + .cast>(); + for (final Map 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); + } } - throw ToolExit(1); } + return deviceIds; + } + + Future> _getDrivers(Directory example) async { + final List drivers = []; - if (pluginsWithoutTests.isNotEmpty) { - print('The following plugins did not run any integration tests:'); - for (final String plugin in pluginsWithoutTests) { - print(' * $plugin'); + 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); + } } - print('If this is intentional, they must be explicitly excluded.'); - throw ToolExit(1); } + return drivers; + } + + File? _getLegacyTestFileForTestDriver(File testDriver) { + final String testName = testDriver.basename.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + final File testFile = testDriver.parent.childFile(testName); - print('All driver tests successful!'); + return testFile.existsSync() ? testFile : null; } - Future _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); - } - if (isIOS) { - return isIosPlugin(plugin); - } - if (isLinux) { - return isLinuxPlugin(plugin); - } - if (isMacos) { - return isMacOsPlugin(plugin); - } - if (isWeb) { - return isWebPlugin(plugin); + Future> _getIntegrationTests(Directory example) async { + final List tests = []; + 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); + } + } } - if (isWindows) { - return isWindowsPlugin(plugin); + return tests; + } + + /// For each file in [targets], uses + /// `flutter drive --driver [driver] --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=', '--browser-name=]` + /// for web + Future> _driveTests( + Directory example, + File driver, + List targets, { + required List deviceFlags, + }) async { + final List failures = []; + + final String enableExperiment = getStringArg(kEnableExperiment); + + for (final File target in targets) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + '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); + } } - // 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); + return failures; } } diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index 3175f7163546..e441a0f68d9e 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:path/path.dart' as p; 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 @@ void main() { runner.addCommand(command); }); - test('driving under folder "test"', () async { - final Directory pluginDirectory = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test/plugin.dart', - ], - platformSupport: { - kPlatformAndroid: PlatformSupport.inline, - kPlatformIos: PlatformSupport.inline, - }, + void setMockFlutterDevicesOutput({ + bool hasIosDevice = true, + bool hasAndroidDevice = true, + }) { + final List devices = [ + if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', + if (hasAndroidDevice) + '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', + ]; + final String output = '''[${devices.join(',')}]'''; + + final MockProcess mockDevicesProcess = MockProcess(); + mockDevicesProcess.exitCodeCompleter.complete(0); + mockDevicesProcess.stdoutController.close(); // ignore: unawaited_futures + processRunner.processToReturn = mockDevicesProcess; + processRunner.resultStdout = output; + } + + test('fails if no platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), ); + }); - final Directory pluginExampleDirectory = - pluginDirectory.childDirectory('example'); + test('fails if multiple platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), + ); + }); + + test('fails for iOS if no iOS devices are present', () async { + setMockFlutterDevicesOutput(hasIosDevice: false); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('No iOS devices'), ]), ); + }); - final String deviceTestPath = p.join('test', 'plugin.dart'); - final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + test('fails if Android if no Android devices are present', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - flutterCommand, - [ - 'drive', - '--driver', - driverTestPath, - '--target', - deviceTestPath - ], - pluginExampleDirectory.path), - ])); + output, + containsAllInOrder([ + contains('No Android devices'), + ]), + ); }); test('driving under folder "test_driver"', () async { @@ -100,16 +144,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -118,10 +161,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -133,6 +180,7 @@ void main() { test('driving under folder "test_driver" when test files are missing"', () async { + setMockFlutterDevicesOutput(); createFakePlugin( 'plugin', packagesDir, @@ -145,13 +193,27 @@ void main() { }, ); - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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 @@ void main() { }, ); - await expectLater( - () => runCapturingPrint(runner, ['drive-examples']), - throwsA(const TypeMatcher())); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (1 example(s) found).'), + contains('No tests ran'), + ]), + ); }); test( @@ -190,16 +265,15 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -208,10 +282,14 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -222,6 +300,8 @@ void main() { flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--driver', driverTestPath, '--target', @@ -244,11 +324,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform linux...'), + contains('No issues found!'), ]), ); @@ -280,10 +359,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -320,11 +398,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform macos...'), + contains('No issues found!'), ]), ); @@ -332,6 +409,7 @@ void main() { // implementation is a no-op. expect(processRunner.recordedCalls, []); }); + test('driving on a macOS plugin', () async { final Directory pluginDirectory = createFakePlugin( 'plugin', @@ -356,10 +434,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -396,11 +473,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -432,10 +507,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -474,11 +548,10 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform windows...'), + contains('No issues found!'), ]), ); @@ -510,10 +583,9 @@ void main() { expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); @@ -537,8 +609,8 @@ void main() { ])); }); - test('driving when plugin does not support mobile is no-op', () async { - createFakePlugin( + test('driving on an Android plugin', () async { + final Directory pluginDirectory = createFakePlugin( 'plugin', packagesDir, extraFiles: [ @@ -546,47 +618,134 @@ void main() { 'example/test_driver/plugin.dart', ], platformSupport: { - kPlatformMacos: PlatformSupport.inline, + kPlatformAndroid: PlatformSupport.inline, }, ); + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); final List output = await runCapturingPrint(runner, [ 'drive-examples', + '--android', ]); expect( output, - orderedEquals([ - '\n==========\nChecking plugin...', - 'Not supported for the target platform; skipping.', - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), ]), ); - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + _fakeAndroidDevice, + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Android is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform android...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty other than the device query. + expect(processRunner.recordedCalls, [ + ProcessCall( + flutterCommand, const ['devices', '--machine'], null), + ]); + }); + + test('driving when plugin does not support iOS is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + 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( + flutterCommand, const ['devices', '--machine'], null), + ]); }); test('platform interface plugins are silently skipped', () async { createFakePlugin('aplugin_platform_interface', packagesDir, examples: []); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - ]); + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--macos']); expect( output, - orderedEquals([ - '\n\n', - 'All driver tests successful!', + containsAllInOrder([ + 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, []); }); @@ -596,7 +755,7 @@ void main() { packagesDir, extraFiles: [ 'example/test_driver/plugin_test.dart', - 'example/test/plugin.dart', + 'example/test_driver/plugin.dart', ], platformSupport: { kPlatformAndroid: PlatformSupport.inline, @@ -607,20 +766,26 @@ void main() { final Directory pluginExampleDirectory = pluginDirectory.childDirectory('example'); + setMockFlutterDevicesOutput(); await runCapturingPrint(runner, [ '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( + flutterCommand, const ['devices', '--machine'], null), ProcessCall( flutterCommand, [ 'drive', + '-d', + _fakeIosDevice, '--enable-experiment=exp1', '--driver', driverTestPath, @@ -630,5 +795,173 @@ void main() { pluginExampleDirectory.path), ])); }); + + test('fails when no example is present', () async { + createFakePlugin( + 'plugin', + packagesDir, + examples: [], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: [ + 'example/test_driver/integration_test.dart', + ], + platformSupport: { + kPlatformWeb: PlatformSupport.inline, + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformMacos: PlatformSupport.inline, + }, + ); + + // Simulate failure from `flutter drive`. + final MockProcess mockDriveProcess = MockProcess(); + mockDriveProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockDriveProcess; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['drive-examples', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + 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( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); }); }