mirror of
https://github.com/flutter/packages.git
synced 2025-08-24 19:42:17 +08:00
[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
This commit is contained in:
@ -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<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.
|
||||
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);
|
||||
}
|
||||
|
||||
String? androidDevice;
|
||||
if (getBoolArg(kPlatformAndroid)) {
|
||||
final List<String> 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<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'],
|
||||
};
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
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 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<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;
|
||||
}
|
||||
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');
|
||||
|
||||
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;
|
||||
}
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
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 (!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);
|
||||
if (!testsRan) {
|
||||
printError('No driver tests were run ($examplesFound example(s) found).');
|
||||
errors.add('No tests ran (use --exclude if this is intentional).');
|
||||
}
|
||||
|
||||
if (pluginsWithoutTests.isNotEmpty) {
|
||||
print('The following plugins did not run any integration tests:');
|
||||
for (final String plugin in pluginsWithoutTests) {
|
||||
print(' * $plugin');
|
||||
}
|
||||
print('If this is intentional, they must be explicitly excluded.');
|
||||
throw ToolExit(1);
|
||||
}
|
||||
|
||||
print('All driver tests successful!');
|
||||
return errors;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
if (isIOS) {
|
||||
return isIosPlugin(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isLinux) {
|
||||
return isLinuxPlugin(plugin);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isMacos) {
|
||||
return isMacOsPlugin(plugin);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isWeb) {
|
||||
return isWebPlugin(plugin);
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (isWindows) {
|
||||
return isWindowsPlugin(plugin);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user