diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 2b754afbd359..d1ec87a4730d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -587,4 +587,34 @@ void main() { expect(platformGoogleMap.buildingsEnabled, true); }); + + testWidgets( + 'Default Android widget is AndroidView', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + expect(find.byType(AndroidView), findsOneWidget); + }, + ); + + // TODO(bparrishMines): Uncomment once https://github.com/flutter/plugins/pull/4017 has landed. + // testWidgets('Use AndroidViewSurface on Android', (WidgetTester tester) async { + // await tester.pumpWidget( + // const Directionality( + // textDirection: TextDirection.ltr, + // child: GoogleMap( + // initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + // ), + // ), + // ); + // + // expect(find.byType(AndroidViewSurface), findsOneWidget); + // }); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index b6603d66fa89..2dc533fe1dfa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.0 + +* Add support for Hybrid Composition when building the Google Maps widget on Android. Set + `MethodChannelGoogleMapsFlutter.useAndroidViewSurface` to `true` to build with Hybrid Composition. + ## 2.0.4 * Preserve the `TileProvider` when copying `TileOverlay`, fixing a diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart index 650a839cb676..300700071102 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/google_maps_flutter_platform_interface.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/method_channel/method_channel_google_maps_flutter.dart' + show MethodChannelGoogleMapsFlutter; export 'src/platform_interface/google_maps_flutter_platform.dart'; export 'src/types/types.dart'; export 'src/events/map_event.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 49029cc3d22d..41aedc759b15 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -8,6 +8,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -441,6 +442,98 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return channel(mapId).invokeMethod('map#takeSnapshot'); } + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// If set to true, the google map widget should be built with + /// [buildViewWithTextDirection] instead of [buildView]. + /// + /// Defaults to false. + bool useAndroidViewSurface = false; + + /// Returns a widget displaying the map view. + /// + /// This method includes a parameter for platforms that require a text + /// direction. For example, this should be used when using hybrid composition + /// on Android. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + if (defaultTargetPlatform == TargetPlatform.android && + useAndroidViewSurface) { + final Map creationParams = { + 'initialCameraPosition': initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(markers), + 'polygonsToAdd': serializePolygonSet(polygons), + 'polylinesToAdd': serializePolylineSet(polylines), + 'circlesToAdd': serializeCircleSet(circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), + }; + return PlatformViewLink( + viewType: 'plugins.flutter.io/google_maps', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final SurfaceAndroidViewController controller = + PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/google_maps', + layoutDirection: textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } + return buildView( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } + @override Widget buildView( int creationId, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 5b278a812a8e..1ea425ea0273 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.0.4 +version: 2.1.0 environment: sdk: '>=2.12.0 <3.0.0' diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 03a3f6fb0b76..697b7c7816dd 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.0.7 + +* Update the README to describe a workaround to the `Uri` query + encoding bug. + ## 6.0.6 * Require `url_launcher_platform_interface` 2.0.3. This fixes an issue diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 31fed9a833f1..20ee0a59caa8 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -10,10 +10,10 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil ## Installation -### iOS +### iOS Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. -Example: +Example: ``` LSApplicationQueriesSchemes @@ -73,25 +73,35 @@ apps installed, so can't open `tel:` or `mailto:` links. ### Encoding URLs -URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html): +URLs must be properly encoded, especially when including spaces or other special +characters. This can be done using the +[`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html). +For example: ```dart -import 'dart:core'; -import 'package:url_launcher/url_launcher.dart'; +String? encodeQueryParameters(Map params) { + return params.entries + .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); +} -final Uri _emailLaunchUri = Uri( +final Uri emailLaunchUri = Uri( scheme: 'mailto', path: 'smith@example.com', - queryParameters: { + query: encodeQueryParameters({ 'subject': 'Example Subject & Symbols are allowed!' - } + }), ); -// ... - -// mailto:smith@example.com?subject=Example+Subject+%26+Symbols+are+allowed%21 -launch(_emailLaunchUri.toString()); +launch(emailLaunchUri.toString()); ``` +**Warning**: For any scheme other than `http` or `https`, you should use the +`query` parameter and the `encodeQueryParameters` function shown above rather +than `Uri`'s `queryParameters` constructor argument, due to +[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + ## Handling missing URL receivers A particular mobile device may not be able to receive all supported URL schemes. @@ -113,4 +123,4 @@ By default, Android opens up a browser when handling URLs. You can pass If you do this for a URL of a page containing JavaScript, make sure to pass in `enableJavaScript: true`, or else the launch method will not work properly. On iOS, the default behavior is to open all web URLs within the app. Everything -else is redirected to the app handler. \ No newline at end of file +else is redirected to the app handler. diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 00cfc218ce9e..a2facbd3adf2 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.6 +version: 6.0.7 environment: sdk: ">=2.12.0 <3.0.0" diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 076c8f69885d..003f0bcda82d 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -7,7 +7,9 @@ import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run Dart analysis on packages. class AnalyzeCommand extends PluginCommand { diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart index 9590aecef98e..61d291d87c68 100644 --- a/script/tool/lib/src/build_examples_command.dart +++ b/script/tool/lib/src/build_examples_command.dart @@ -9,7 +9,10 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// Key for IPA. const String kIpa = 'ipa'; diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart deleted file mode 100644 index 5d653ad0ed20..000000000000 --- a/script/tool/lib/src/common.dart +++ /dev/null @@ -1,781 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; -import 'dart:math'; - -import 'package:args/command_runner.dart'; -import 'package:colorize/colorize.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml/yaml.dart'; - -/// The signature for a print handler for commands that allow overriding the -/// print destination. -typedef Print = void Function(Object? object); - -/// Key for windows platform. -const String kPlatformFlagWindows = 'windows'; - -/// Key for macos platform. -const String kPlatformFlagMacos = 'macos'; - -/// Key for linux platform. -const String kPlatformFlagLinux = 'linux'; - -/// Key for IPA (iOS) platform. -const String kPlatformFlagIos = 'ios'; - -/// Key for APK (Android) platform. -const String kPlatformFlagAndroid = 'android'; - -/// Key for Web platform. -const String kPlatformFlagWeb = 'web'; - -/// Key for enable experiment. -const String kEnableExperiment = 'enable-experiment'; - -/// Returns whether the given directory contains a Flutter package. -bool isFlutterPackage(FileSystemEntity entity) { - if (entity is! Directory) { - return false; - } - - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; - if (dependencies == null) { - return false; - } - return dependencies.containsKey('flutter'); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Possible plugin support options for a platform. -enum PlatformSupport { - /// The platform has an implementation in the package. - inline, - - /// The platform has an endorsed federated implementation in another package. - federated, -} - -/// Returns whether the given directory contains a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// -/// If [requiredMode] is provided, the plugin must have the given type of -/// implementation in order to return true. -bool pluginSupportsPlatform(String platform, FileSystemEntity entity, - {PlatformSupport? requiredMode}) { - assert(platform == kPlatformFlagIos || - platform == kPlatformFlagAndroid || - platform == kPlatformFlagWeb || - platform == kPlatformFlagMacos || - platform == kPlatformFlagWindows || - platform == kPlatformFlagLinux); - if (entity is! Directory) { - return false; - } - - try { - final File pubspecFile = entity.childFile('pubspec.yaml'); - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; - if (flutterSection == null) { - return false; - } - final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; - if (pluginSection == null) { - return false; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - // Legacy plugin specs are assumed to support iOS and Android. They are - // never federated. - if (requiredMode == PlatformSupport.federated) { - return false; - } - if (!pluginSection.containsKey('platforms')) { - return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; - } - return false; - } - final YamlMap? platformEntry = platforms[platform] as YamlMap?; - if (platformEntry == null) { - return false; - } - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - final bool federated = platformEntry.containsKey('default_package'); - return requiredMode == null || - federated == (requiredMode == PlatformSupport.federated); - } on FileSystemException { - return false; - } on YamlException { - return false; - } -} - -/// Returns whether the given directory contains a Flutter Android plugin. -bool isAndroidPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagAndroid, entity); -} - -/// Returns whether the given directory contains a Flutter iOS plugin. -bool isIosPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagIos, entity); -} - -/// Returns whether the given directory contains a Flutter web plugin. -bool isWebPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWeb, entity); -} - -/// Returns whether the given directory contains a Flutter Windows plugin. -bool isWindowsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagWindows, entity); -} - -/// Returns whether the given directory contains a Flutter macOS plugin. -bool isMacOsPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagMacos, entity); -} - -/// Returns whether the given directory contains a Flutter linux plugin. -bool isLinuxPlugin(FileSystemEntity entity) { - return pluginSupportsPlatform(kPlatformFlagLinux, entity); -} - -/// Prints `errorMessage` in red. -void printError(String errorMessage) { - final Colorize redError = Colorize(errorMessage)..red(); - print(redError); -} - -/// Error thrown when a command needs to exit with a non-zero exit code. -class ToolExit extends Error { - /// Creates a tool exit with the given [exitCode]. - ToolExit(this.exitCode); - - /// The code that the process should exit with. - final int exitCode; -} - -/// Interface definition for all commands in this tool. -abstract class PluginCommand extends Command { - /// Creates a command to operate on [packagesDir] with the given environment. - PluginCommand( - this.packagesDir, { - this.processRunner = const ProcessRunner(), - this.gitDir, - }) { - argParser.addMultiOption( - _pluginsArg, - splitCommas: true, - help: - 'Specifies which plugins the command should run on (before sharding).', - valueHelp: 'plugin1,plugin2,...', - ); - argParser.addOption( - _shardIndexArg, - help: 'Specifies the zero-based index of the shard to ' - 'which the command applies.', - valueHelp: 'i', - defaultsTo: '0', - ); - argParser.addOption( - _shardCountArg, - help: 'Specifies the number of shards into which plugins are divided.', - valueHelp: 'n', - defaultsTo: '1', - ); - argParser.addMultiOption( - _excludeArg, - abbr: 'e', - help: 'Exclude packages from this command.', - defaultsTo: [], - ); - argParser.addFlag(_runOnChangedPackagesArg, - help: 'Run the command on changed packages/plugins.\n' - 'If the $_pluginsArg is specified, this flag is ignored.\n' - 'If no packages have changed, or if there have been changes that may\n' - 'affect all packages, the command runs on all packages.\n' - 'The packages excluded with $_excludeArg is also excluded even if changed.\n' - 'See $_kBaseSha if a custom base is needed to determine the diff.'); - argParser.addOption(_kBaseSha, - help: 'The base sha used to determine git diff. \n' - 'This is useful when $_runOnChangedPackagesArg is specified.\n' - 'If not specified, merge-base is used as base sha.'); - } - - static const String _pluginsArg = 'plugins'; - static const String _shardIndexArg = 'shardIndex'; - static const String _shardCountArg = 'shardCount'; - static const String _excludeArg = 'exclude'; - static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; - static const String _kBaseSha = 'base-sha'; - - /// The directory containing the plugin packages. - final Directory packagesDir; - - /// The process runner. - /// - /// This can be overridden for testing. - final ProcessRunner processRunner; - - /// The git directory to use. By default it uses the parent directory. - /// - /// This can be mocked for testing. - final GitDir? gitDir; - - int? _shardIndex; - int? _shardCount; - - /// The shard of the overall command execution that this instance should run. - int get shardIndex { - if (_shardIndex == null) { - _checkSharding(); - } - return _shardIndex!; - } - - /// The number of shards this command is divided into. - int get shardCount { - if (_shardCount == null) { - _checkSharding(); - } - return _shardCount!; - } - - /// Convenience accessor for boolean arguments. - bool getBoolArg(String key) { - return (argResults![key] as bool?) ?? false; - } - - /// Convenience accessor for String arguments. - String getStringArg(String key) { - return (argResults![key] as String?) ?? ''; - } - - /// Convenience accessor for List arguments. - List getStringListArg(String key) { - return (argResults![key] as List?) ?? []; - } - - void _checkSharding() { - final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); - final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); - if (shardIndex == null) { - usageException('$_shardIndexArg must be an integer'); - } - if (shardCount == null) { - usageException('$_shardCountArg must be an integer'); - } - if (shardCount < 1) { - usageException('$_shardCountArg must be positive'); - } - if (shardIndex < 0 || shardCount <= shardIndex) { - usageException( - '$_shardIndexArg must be in the half-open range [0..$shardCount['); - } - _shardIndex = shardIndex; - _shardCount = shardCount; - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution. - Stream getPlugins() async* { - // To avoid assuming consistency of `Directory.list` across command - // invocations, we collect and sort the plugin folders before sharding. - // This is considered an implementation detail which is why the API still - // uses streams. - final List allPlugins = await _getAllPlugins().toList(); - allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); - // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. - // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. - // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. - final int shardSize = allPlugins.length ~/ shardCount + - (allPlugins.length % shardCount == 0 ? 0 : 1); - final int start = min(shardIndex * shardSize, allPlugins.length); - final int end = min(start + shardSize, allPlugins.length); - - for (final Directory plugin in allPlugins.sublist(start, end)) { - yield plugin; - } - } - - /// Returns the root Dart package folders of the plugins involved in this - /// command execution, assuming there is only one shard. - /// - /// Plugin packages can exist in the following places relative to the packages - /// directory: - /// - /// 1. As a Dart package in a directory which is a direct child of the - /// packages directory. This is a plugin where all of the implementations - /// exist in a single Dart package. - /// 2. Several plugin packages may live in a directory which is a direct - /// child of the packages directory. This directory groups several Dart - /// packages which implement a single plugin. This directory contains a - /// "client library" package, which declares the API for the plugin, as - /// well as one or more platform-specific implementations. - /// 3./4. Either of the above, but in a third_party/packages/ directory that - /// is a sibling of the packages directory. This is used for a small number - /// of packages in the flutter/packages repository. - Stream _getAllPlugins() async* { - Set plugins = Set.from(getStringListArg(_pluginsArg)); - final Set excludedPlugins = - Set.from(getStringListArg(_excludeArg)); - final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); - if (plugins.isEmpty && - runOnChangedPackages && - !(await _changesRequireFullTest())) { - plugins = await _getChangedPackages(); - } - - final Directory thirdPartyPackagesDirectory = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - - for (final Directory dir in [ - packagesDir, - if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, - ]) { - await for (final FileSystemEntity entity - in dir.list(followLinks: false)) { - // A top-level Dart package is a plugin package. - if (_isDartPackage(entity)) { - if (!excludedPlugins.contains(entity.basename) && - (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { - yield entity as Directory; - } - } else if (entity is Directory) { - // Look for Dart packages under this top-level directory. - await for (final FileSystemEntity subdir - in entity.list(followLinks: false)) { - if (_isDartPackage(subdir)) { - // If --plugin=my_plugin is passed, then match all federated - // plugins under 'my_plugin'. Also match if the exact plugin is - // passed. - final String relativePath = - p.relative(subdir.path, from: dir.path); - final String packageName = p.basename(subdir.path); - final String basenamePath = p.basename(entity.path); - if (!excludedPlugins.contains(basenamePath) && - !excludedPlugins.contains(packageName) && - !excludedPlugins.contains(relativePath) && - (plugins.isEmpty || - plugins.contains(relativePath) || - plugins.contains(basenamePath))) { - yield subdir as Directory; - } - } - } - } - } - } - } - - /// Returns the example Dart package folders of the plugins involved in this - /// command execution. - Stream getExamples() => - getPlugins().expand(getExamplesForPlugin); - - /// Returns all Dart package folders (typically, plugin + example) of the - /// plugins involved in this command execution. - Stream getPackages() async* { - await for (final Directory plugin in getPlugins()) { - yield plugin; - yield* plugin - .list(recursive: true, followLinks: false) - .where(_isDartPackage) - .cast(); - } - } - - /// Returns the files contained, recursively, within the plugins - /// involved in this command execution. - Stream getFiles() { - return getPlugins().asyncExpand((Directory folder) => folder - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast()); - } - - /// Returns whether the specified entity is a directory containing a - /// `pubspec.yaml` file. - bool _isDartPackage(FileSystemEntity entity) { - return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); - } - - /// Returns the example Dart packages contained in the specified plugin, or - /// an empty List, if the plugin has no examples. - Iterable getExamplesForPlugin(Directory plugin) { - final Directory exampleFolder = plugin.childDirectory('example'); - if (!exampleFolder.existsSync()) { - return []; - } - if (isFlutterPackage(exampleFolder)) { - return [exampleFolder]; - } - // Only look at the subdirectories of the example directory if the example - // directory itself is not a Dart package, and only look one level below the - // example directory for other dart packages. - return exampleFolder - .listSync() - .where((FileSystemEntity entity) => isFlutterPackage(entity)) - .cast(); - } - - /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. - /// - /// Throws tool exit if [gitDir] nor root directory is a git directory. - Future retrieveVersionFinder() async { - final String rootDir = packagesDir.parent.absolute.path; - final String baseSha = getStringArg(_kBaseSha); - - GitDir? baseGitDir = gitDir; - if (baseGitDir == null) { - if (!await GitDir.isGitDir(rootDir)) { - printError( - '$rootDir is not a valid Git repository.', - ); - throw ToolExit(2); - } - baseGitDir = await GitDir.fromExisting(rootDir); - } - - final GitVersionFinder gitVersionFinder = - GitVersionFinder(baseGitDir, baseSha); - return gitVersionFinder; - } - - // Returns packages that have been changed relative to the git base. - Future> _getChangedPackages() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); - final Set packages = {}; - for (final String path in allChangedFiles) { - final List pathComponents = path.split('/'); - final int packagesIndex = - pathComponents.indexWhere((String element) => element == 'packages'); - if (packagesIndex != -1) { - packages.add(pathComponents[packagesIndex + 1]); - } - } - if (packages.isEmpty) { - print('No changed packages.'); - } else { - final String changedPackages = packages.join(','); - print('Changed packages: $changedPackages'); - } - return packages; - } - - // Returns true if one or more files changed that have the potential to affect - // any plugin (e.g., CI script changes). - Future _changesRequireFullTest() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - - const List specialFiles = [ - '.ci.yaml', // LUCI config. - '.cirrus.yml', // Cirrus config. - '.clang-format', // ObjC and C/C++ formatting options. - 'analysis_options.yaml', // Dart analysis settings. - ]; - const List specialDirectories = [ - '.ci/', // Support files for CI. - 'script/', // This tool, and its wrapper scripts. - ]; - // Directory entries must end with / to avoid over-matching, since the - // check below is done via string prefixing. - assert(specialDirectories.every((String dir) => dir.endsWith('/'))); - - final List allChangedFiles = - await gitVersionFinder.getChangedFiles(); - return allChangedFiles.any((String path) => - specialFiles.contains(path) || - specialDirectories.any((String dir) => path.startsWith(dir))); - } -} - -/// A class used to run processes. -/// -/// We use this instead of directly running the process so it can be overridden -/// in tests. -class ProcessRunner { - /// Creates a new process runner. - const ProcessRunner(); - - /// Run the [executable] with [args] and stream output to stderr and stdout. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// - /// Returns the exit code of the [executable]. - Future runAndStream( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - }) async { - print( - 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDir?.path); - await io.stdout.addStream(process.stdout); - await io.stderr.addStream(process.stderr); - if (exitOnError && await process.exitCode != 0) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error See above for details.'); - throw ToolExit(await process.exitCode); - } - return process.exitCode; - } - - /// Run the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// If [exitOnError] is set to `true`, then this will throw an error if - /// the [executable] terminates with a non-zero exit code. - /// Defaults to `false`. - /// - /// If [logOnError] is set to `true`, it will print a formatted message about the error. - /// Defaults to `false` - /// - /// Returns the [io.ProcessResult] of the [executable]. - Future run(String executable, List args, - {Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding}) async { - final io.ProcessResult result = await io.Process.run(executable, args, - workingDirectory: workingDir?.path, - stdoutEncoding: stdoutEncoding, - stderrEncoding: stderrEncoding); - if (result.exitCode != 0) { - if (logOnError) { - final String error = - _getErrorString(executable, args, workingDir: workingDir); - print('$error Stderr:\n${result.stdout}'); - } - if (exitOnError) { - throw ToolExit(result.exitCode); - } - } - return result; - } - - /// Starts the [executable] with [args]. - /// - /// The current working directory of [executable] can be overridden by - /// passing [workingDir]. - /// - /// Returns the started [io.Process]. - Future start(String executable, List args, - {Directory? workingDirectory}) async { - final io.Process process = await io.Process.start(executable, args, - workingDirectory: workingDirectory?.path); - return process; - } - - String _getErrorString(String executable, List args, - {Directory? workingDir}) { - final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; - return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; - } -} - -/// Finding version of [package] that is published on pub. -class PubVersionFinder { - /// Constructor. - /// - /// Note: you should manually close the [httpClient] when done using the finder. - PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); - - /// The default pub host to use. - static const String defaultPubHost = 'https://pub.dev'; - - /// The pub host url, defaults to `https://pub.dev`. - final String pubHost; - - /// The http client. - /// - /// You should manually close this client when done using this finder. - final http.Client httpClient; - - /// Get the package version on pub. - Future getPackageVersion( - {required String package}) async { - assert(package.isNotEmpty); - final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$package.json'); - final http.Response response = await httpClient.get(url); - - if (response.statusCode == 404) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.noPackageFound, - httpResponse: response); - } else if (response.statusCode != 200) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.fail, - httpResponse: response); - } - final List versions = - (json.decode(response.body)['versions'] as List) - .map((final dynamic versionString) => - Version.parse(versionString as String)) - .toList(); - - return PubVersionFinderResponse( - versions: versions, - result: PubVersionFinderResult.success, - httpResponse: response); - } -} - -/// Represents a response for [PubVersionFinder]. -class PubVersionFinderResponse { - /// Constructor. - PubVersionFinderResponse( - {required this.versions, - required this.result, - required this.httpResponse}) { - if (versions.isNotEmpty) { - versions.sort((Version a, Version b) { - // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. - // https://github.com/flutter/flutter/issues/82222 - return b.compareTo(a); - }); - } - } - - /// The versions found in [PubVersionFinder]. - /// - /// This is sorted by largest to smallest, so the first element in the list is the largest version. - /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. - final List versions; - - /// The result of the version finder. - final PubVersionFinderResult result; - - /// The response object of the http request. - final http.Response httpResponse; -} - -/// An enum representing the result of [PubVersionFinder]. -enum PubVersionFinderResult { - /// The version finder successfully found a version. - success, - - /// The version finder failed to find a valid version. - /// - /// This might due to http connection errors or user errors. - fail, - - /// The version finder failed to locate the package. - /// - /// This indicates the package is new. - noPackageFound, -} - -/// Finding diffs based on `baseGitDir` and `baseSha`. -class GitVersionFinder { - /// Constructor - GitVersionFinder(this.baseGitDir, this.baseSha); - - /// The top level directory of the git repo. - /// - /// That is where the .git/ folder exists. - final GitDir baseGitDir; - - /// The base sha used to get diff. - final String? baseSha; - - static bool _isPubspec(String file) { - return file.trim().endsWith('pubspec.yaml'); - } - - /// Get a list of all the pubspec.yaml file that is changed. - Future> getChangedPubSpecs() async { - return (await getChangedFiles()).where(_isPubspec).toList(); - } - - /// Get a list of all the changed files. - Future> getChangedFiles() async { - final String baseSha = await _getBaseSha(); - final io.ProcessResult changedFilesCommand = await baseGitDir - .runCommand(['diff', '--name-only', baseSha, 'HEAD']); - print('Determine diff with base sha: $baseSha'); - final String changedFilesStdout = changedFilesCommand.stdout.toString(); - if (changedFilesStdout.isEmpty) { - return []; - } - final List changedFiles = changedFilesStdout.split('\n') - ..removeWhere((String element) => element.isEmpty); - return changedFiles.toList(); - } - - /// Get the package version specified in the pubspec file in `pubspecPath` and - /// at the revision of `gitRef` (defaulting to the base if not provided). - Future getPackageVersion(String pubspecPath, - {String? gitRef}) async { - final String ref = gitRef ?? (await _getBaseSha()); - - io.ProcessResult gitShow; - try { - gitShow = - await baseGitDir.runCommand(['show', '$ref:$pubspecPath']); - } on io.ProcessException { - return null; - } - final String fileContent = gitShow.stdout as String; - final String? versionString = loadYaml(fileContent)['version'] as String?; - return versionString == null ? null : Version.parse(versionString); - } - - Future _getBaseSha() async { - if (baseSha != null && baseSha!.isNotEmpty) { - return baseSha!; - } - - io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( - ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], - throwOnError: false); - if (baseShaFromMergeBase.stderr != null || - baseShaFromMergeBase.stdout == null) { - baseShaFromMergeBase = await baseGitDir - .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); - } - return (baseShaFromMergeBase.stdout as String).trim(); - } -} diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart new file mode 100644 index 000000000000..4788b9fa9143 --- /dev/null +++ b/script/tool/lib/src/common/core.dart @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; + +/// The signature for a print handler for commands that allow overriding the +/// print destination. +typedef Print = void Function(Object? object); + +/// Key for windows platform. +const String kPlatformFlagWindows = 'windows'; + +/// Key for macos platform. +const String kPlatformFlagMacos = 'macos'; + +/// Key for linux platform. +const String kPlatformFlagLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kPlatformFlagIos = 'ios'; + +/// Key for APK (Android) platform. +const String kPlatformFlagAndroid = 'android'; + +/// Key for Web platform. +const String kPlatformFlagWeb = 'web'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity) { + if (entity is! Directory) { + return false; + } + + try { + final File pubspecFile = entity.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? dependencies = pubspecYaml['dependencies'] as YamlMap?; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Prints `errorMessage` in red. +void printError(String errorMessage) { + final Colorize redError = Colorize(errorMessage)..red(); + print(redError); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + /// Creates a tool exit with the given [exitCode]. + ToolExit(this.exitCode); + + /// The code that the process should exit with. + final int exitCode; +} diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart new file mode 100644 index 000000000000..2c9519e7a856 --- /dev/null +++ b/script/tool/lib/src/common/git_version_finder.dart @@ -0,0 +1,81 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; + +/// Finding diffs based on `baseGitDir` and `baseSha`. +class GitVersionFinder { + /// Constructor + GitVersionFinder(this.baseGitDir, this.baseSha); + + /// The top level directory of the git repo. + /// + /// That is where the .git/ folder exists. + final GitDir baseGitDir; + + /// The base sha used to get diff. + final String? baseSha; + + static bool _isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + /// Get a list of all the pubspec.yaml file that is changed. + Future> getChangedPubSpecs() async { + return (await getChangedFiles()).where(_isPubspec).toList(); + } + + /// Get a list of all the changed files. + Future> getChangedFiles() async { + final String baseSha = await _getBaseSha(); + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', baseSha, 'HEAD']); + print('Determine diff with base sha: $baseSha'); + final String changedFilesStdout = changedFilesCommand.stdout.toString(); + if (changedFilesStdout.isEmpty) { + return []; + } + final List changedFiles = changedFilesStdout.split('\n') + ..removeWhere((String element) => element.isEmpty); + return changedFiles.toList(); + } + + /// Get the package version specified in the pubspec file in `pubspecPath` and + /// at the revision of `gitRef` (defaulting to the base if not provided). + Future getPackageVersion(String pubspecPath, + {String? gitRef}) async { + final String ref = gitRef ?? (await _getBaseSha()); + + io.ProcessResult gitShow; + try { + gitShow = + await baseGitDir.runCommand(['show', '$ref:$pubspecPath']); + } on io.ProcessException { + return null; + } + final String fileContent = gitShow.stdout as String; + final String? versionString = loadYaml(fileContent)['version'] as String?; + return versionString == null ? null : Version.parse(versionString); + } + + Future _getBaseSha() async { + if (baseSha != null && baseSha!.isNotEmpty) { + return baseSha!; + } + + io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + throwOnError: false); + if (baseShaFromMergeBase.stderr != null || + baseShaFromMergeBase.stdout == null) { + baseShaFromMergeBase = await baseGitDir + .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); + } + return (baseShaFromMergeBase.stdout as String).trim(); + } +} diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart new file mode 100644 index 000000000000..1ab9d8dcc6e0 --- /dev/null +++ b/script/tool/lib/src/common/plugin_command.dart @@ -0,0 +1,353 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; +import 'git_version_finder.dart'; +import 'process_runner.dart'; + +/// Interface definition for all commands in this tool. +abstract class PluginCommand extends Command { + /// Creates a command to operate on [packagesDir] with the given environment. + PluginCommand( + this.packagesDir, { + this.processRunner = const ProcessRunner(), + this.gitDir, + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + argParser.addFlag(_runOnChangedPackagesArg, + help: 'Run the command on changed packages/plugins.\n' + 'If the $_pluginsArg is specified, this flag is ignored.\n' + 'If no packages have changed, or if there have been changes that may\n' + 'affect all packages, the command runs on all packages.\n' + 'The packages excluded with $_excludeArg is also excluded even if changed.\n' + 'See $_kBaseSha if a custom base is needed to determine the diff.'); + argParser.addOption(_kBaseSha, + help: 'The base sha used to determine git diff. \n' + 'This is useful when $_runOnChangedPackagesArg is specified.\n' + 'If not specified, merge-base is used as base sha.'); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _kBaseSha = 'base-sha'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir? gitDir; + + int? _shardIndex; + int? _shardCount; + + /// The shard of the overall command execution that this instance should run. + int get shardIndex { + if (_shardIndex == null) { + _checkSharding(); + } + return _shardIndex!; + } + + /// The number of shards this command is divided into. + int get shardCount { + if (_shardCount == null) { + _checkSharding(); + } + return _shardCount!; + } + + /// Convenience accessor for boolean arguments. + bool getBoolArg(String key) { + return (argResults![key] as bool?) ?? false; + } + + /// Convenience accessor for String arguments. + String getStringArg(String key) { + return (argResults![key] as String?) ?? ''; + } + + /// Convenience accessor for List arguments. + List getStringListArg(String key) { + return (argResults![key] as List?) ?? []; + } + + void _checkSharding() { + final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); + final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (final Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in the following places relative to the packages + /// directory: + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + /// 3./4. Either of the above, but in a third_party/packages/ directory that + /// is a sibling of the packages directory. This is used for a small number + /// of packages in the flutter/packages repository. + Stream _getAllPlugins() async* { + Set plugins = Set.from(getStringListArg(_pluginsArg)); + final Set excludedPlugins = + Set.from(getStringListArg(_excludeArg)); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); + if (plugins.isEmpty && + runOnChangedPackages && + !(await _changesRequireFullTest())) { + plugins = await _getChangedPackages(); + } + + final Directory thirdPartyPackagesDirectory = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + + for (final Directory dir in [ + packagesDir, + if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, + ]) { + await for (final FileSystemEntity entity + in dir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity as Directory; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (final FileSystemEntity subdir + in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: dir.path); + final String packageName = p.basename(subdir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(packageName) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir as Directory; + } + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (final Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = plugin.childDirectory('example'); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + .cast(); + } + + /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// + /// Throws tool exit if [gitDir] nor root directory is a git directory. + Future retrieveVersionFinder() async { + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = getStringArg(_kBaseSha); + + GitDir? baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + printError( + '$rootDir is not a valid Git repository.', + ); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + return gitVersionFinder; + } + + // Returns packages that have been changed relative to the git base. + Future> _getChangedPackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + final Set packages = {}; + for (final String path in allChangedFiles) { + final List pathComponents = path.split('/'); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + } + if (packages.isEmpty) { + print('No changed packages.'); + } else { + final String changedPackages = packages.join(','); + print('Changed packages: $changedPackages'); + } + return packages; + } + + // Returns true if one or more files changed that have the potential to affect + // any plugin (e.g., CI script changes). + Future _changesRequireFullTest() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + const List specialFiles = [ + '.ci.yaml', // LUCI config. + '.cirrus.yml', // Cirrus config. + '.clang-format', // ObjC and C/C++ formatting options. + 'analysis_options.yaml', // Dart analysis settings. + ]; + const List specialDirectories = [ + '.ci/', // Support files for CI. + 'script/', // This tool, and its wrapper scripts. + ]; + // Directory entries must end with / to avoid over-matching, since the + // check below is done via string prefixing. + assert(specialDirectories.every((String dir) => dir.endsWith('/'))); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + return allChangedFiles.any((String path) => + specialFiles.contains(path) || + specialDirectories.any((String dir) => path.startsWith(dir))); + } +} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart new file mode 100644 index 000000000000..b6ac433db2e2 --- /dev/null +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; + +import 'core.dart'; + +/// Possible plugin support options for a platform. +enum PlatformSupport { + /// The platform has an implementation in the package. + inline, + + /// The platform has an endorsed federated implementation in another package. + federated, +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// +/// If [requiredMode] is provided, the plugin must have the given type of +/// implementation in order to return true. +bool pluginSupportsPlatform(String platform, FileSystemEntity entity, + {PlatformSupport? requiredMode}) { + assert(platform == kPlatformFlagIos || + platform == kPlatformFlagAndroid || + platform == kPlatformFlagWeb || + platform == kPlatformFlagMacos || + platform == kPlatformFlagWindows || + platform == kPlatformFlagLinux); + if (entity is! Directory) { + return false; + } + + try { + final File pubspecFile = entity.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return false; + } + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; + if (pluginSection == null) { + return false; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. They are + // never federated. + if (requiredMode == PlatformSupport.federated) { + return false; + } + if (!pluginSection.containsKey('platforms')) { + return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; + } + return false; + } + final YamlMap? platformEntry = platforms[platform] as YamlMap?; + if (platformEntry == null) { + return false; + } + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + final bool federated = platformEntry.containsKey('default_package'); + return requiredMode == null || + federated == (requiredMode == PlatformSupport.federated); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagAndroid, entity); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagIos, entity); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWeb, entity); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWindows, entity); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagMacos, entity); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagLinux, entity); +} diff --git a/script/tool/lib/src/common/process_runner.dart b/script/tool/lib/src/common/process_runner.dart new file mode 100644 index 000000000000..429761ead3b8 --- /dev/null +++ b/script/tool/lib/src/common/process_runner.dart @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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' as io; + +import 'package:file/file.dart'; + +import 'core.dart'; + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + /// Creates a new process runner. + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// Defaults to `false`. + /// + /// If [logOnError] is set to `true`, it will print a formatted message about the error. + /// Defaults to `false` + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding}) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + if (result.exitCode != 0) { + if (logOnError) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + } + if (exitOnError) { + throw ToolExit(result.exitCode); + } + } + return result; + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory? workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + String _getErrorString(String executable, List args, + {Directory? workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart new file mode 100644 index 000000000000..ebac473de7ac --- /dev/null +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -0,0 +1,103 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:pub_semver/pub_semver.dart'; + +/// Finding version of [package] that is published on pub. +class PubVersionFinder { + /// Constructor. + /// + /// Note: you should manually close the [httpClient] when done using the finder. + PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); + + /// The default pub host to use. + static const String defaultPubHost = 'https://pub.dev'; + + /// The pub host url, defaults to `https://pub.dev`. + final String pubHost; + + /// The http client. + /// + /// You should manually close this client when done using this finder. + final http.Client httpClient; + + /// Get the package version on pub. + Future getPackageVersion( + {required String package}) async { + assert(package.isNotEmpty); + final Uri pubHostUri = Uri.parse(pubHost); + final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final http.Response response = await httpClient.get(url); + + if (response.statusCode == 404) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.noPackageFound, + httpResponse: response); + } else if (response.statusCode != 200) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.fail, + httpResponse: response); + } + final List versions = + (json.decode(response.body)['versions'] as List) + .map((final dynamic versionString) => + Version.parse(versionString as String)) + .toList(); + + return PubVersionFinderResponse( + versions: versions, + result: PubVersionFinderResult.success, + httpResponse: response); + } +} + +/// Represents a response for [PubVersionFinder]. +class PubVersionFinderResponse { + /// Constructor. + PubVersionFinderResponse( + {required this.versions, + required this.result, + required this.httpResponse}) { + if (versions.isNotEmpty) { + versions.sort((Version a, Version b) { + // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. + // https://github.com/flutter/flutter/issues/82222 + return b.compareTo(a); + }); + } + } + + /// The versions found in [PubVersionFinder]. + /// + /// This is sorted by largest to smallest, so the first element in the list is the largest version. + /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. + final List versions; + + /// The result of the version finder. + final PubVersionFinderResult result; + + /// The response object of the http request. + final http.Response httpResponse; +} + +/// An enum representing the result of [PubVersionFinder]. +enum PubVersionFinderResult { + /// The version finder successfully found a version. + success, + + /// The version finder failed to find a valid version. + /// + /// This might due to http connection errors or user errors. + fail, + + /// The version finder failed to locate the package. + /// + /// This indicates the package is new. + noPackageFound, +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart index cd5b85e45ac0..fab41bcf4ec4 100644 --- a/script/tool/lib/src/create_all_plugins_app_command.dart +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -8,7 +8,8 @@ import 'package:file/file.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; /// A command to create an application that builds all in a single application. class CreateAllPluginsAppCommand extends PluginCommand { diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart index 14dfede5b2f1..b6576cd13ba8 100644 --- a/script/tool/lib/src/drive_examples_command.dart +++ b/script/tool/lib/src/drive_examples_command.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; + +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// A command to run the example applications for packages via Flutter driver. class DriveExamplesCommand extends PluginCommand { diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart index 741d8569322b..b4f5e92933c6 100644 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -9,7 +9,9 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:uuid/uuid.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run tests via Firebase test lab. class FirebaseTestLabCommand extends PluginCommand { diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart index 1ef41f82bb2c..5f060d715bfd 100644 --- a/script/tool/lib/src/format_command.dart +++ b/script/tool/lib/src/format_command.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; @@ -11,7 +10,9 @@ import 'package:http/http.dart' as http; import 'package:path/path.dart' as p; import 'package:quiver/iterables.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; final Uri _googleFormatterUrl = Uri.https('github.com', '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart index d1366ea7636a..d7e453b6ad74 100644 --- a/script/tool/lib/src/java_test_command.dart +++ b/script/tool/lib/src/java_test_command.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to run the Java tests of Android plugins. class JavaTestCommand extends PluginCommand { diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 805c3ab9f900..4ea8a1e09392 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; const Set _codeFileExtensions = { '.c', diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart index 364653bd13ba..5e86d2be40b8 100644 --- a/script/tool/lib/src/lint_podspecs_command.dart +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -9,7 +9,9 @@ import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// Lint the CocoaPod podspecs and run unit tests. /// diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart index f6b186e7ba2f..39515cf686b0 100644 --- a/script/tool/lib/src/list_command.dart +++ b/script/tool/lib/src/list_command.dart @@ -2,11 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; -import 'common.dart'; +import 'common/plugin_command.dart'; /// A command to list different types of repository content. class ListCommand extends PluginCommand { diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index a7603122186a..f397a04aa663 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -10,7 +10,7 @@ import 'package:file/local.dart'; import 'analyze_command.dart'; import 'build_examples_command.dart'; -import 'common.dart'; +import 'common/core.dart'; import 'create_all_plugins_app_command.dart'; import 'drive_examples_command.dart'; import 'firebase_test_lab_command.dart'; diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index b77eceecbf41..82a76609e98b 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -12,7 +12,10 @@ import 'package:http/http.dart' as http; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; /// A command to check that packages are publishable via 'dart publish'. class PublishCheckCommand extends PluginCommand { diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart index 1e7c15029846..70ec75bc7b76 100644 --- a/script/tool/lib/src/publish_plugin_command.dart +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -14,7 +14,10 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:yaml/yaml.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; @immutable class _RemoteInfo { diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index 878b683dbbb8..480d3a4c1190 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -2,14 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:path/path.dart' as p; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; /// A command to enforce pubspec conventions across the repository. /// diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 0174b986eb63..b7bf261caa8a 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -2,12 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; /// A command to run Dart unit tests for packages. class TestCommand extends PluginCommand { diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index 6baa38e465a2..5e9f55333f8e 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:file/file.dart'; import 'package:git/git.dart'; import 'package:http/http.dart' as http; @@ -11,7 +9,11 @@ import 'package:meta/meta.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; -import 'common.dart'; +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; /// Categories of version change types. enum NextVersionType { diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart index 288851ca7edf..77e5659df3f6 100644 --- a/script/tool/lib/src/xctest_command.dart +++ b/script/tool/lib/src/xctest_command.dart @@ -2,14 +2,16 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; import 'package:file/file.dart'; import 'package:path/path.dart' as p; -import 'common.dart'; +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; const String _kiOSDestination = 'ios-destination'; const String _kXcodeBuildCommand = 'xcodebuild'; diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index ec627f25864c..1ef4fdc44b42 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -6,7 +6,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:test/test.dart'; import 'mocks.dart'; diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart new file mode 100644 index 000000000000..f1f40b5e0035 --- /dev/null +++ b/script/tool/test/common/git_version_finder_test.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'plugin_command_test.mocks.dart'; + +void main() { + late List?> gitDirCommands; + late String gitDiffResponse; + late MockGitDir gitDir; + String? mergeBaseResponse; + + setUp(() { + gitDirCommands = ?>[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0] as List?); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout as String?) + .thenReturn(mergeBaseResponse); + } + return Future.value(mockProcessResult); + }); + }); + + test('No git diff should result no files changed', () async { + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, isEmpty); + }); + + test('get correct files changed based on git diff', () async { + gitDiffResponse = ''' +file1/file1.cc +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); + }); + + test('get correct pubspec change based on git diff', () async { + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedPubSpecs(); + + expect(changedFiles, equals(['file1/pubspec.yaml'])); + }); + + test('use correct base sha if not specified', () async { + mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + + final GitVersionFinder finder = GitVersionFinder(gitDir, null); + await finder.getChangedFiles(); + verify(gitDir.runCommand( + ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); + }); + + test('use correct base sha if specified', () async { + const String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(); + verify(gitDir + .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common_test.dart b/script/tool/test/common/plugin_command_test.dart similarity index 51% rename from script/tool/test/common_test.dart rename to script/tool/test/common/plugin_command_test.dart index a51182d91ff8..58d202e19920 100644 --- a/script/tool/test/common_test.dart +++ b/script/tool/test/common/plugin_command_test.dart @@ -2,24 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common_test.mocks.dart'; -import 'util.dart'; +import '../util.dart'; +import 'plugin_command_test.mocks.dart'; @GenerateMocks([GitDir]) void main() { @@ -362,355 +358,6 @@ packages/plugin3/plugin3.dart }); }); }); - - group('$GitVersionFinder', () { - late FileSystem fileSystem; - late List?> gitDirCommands; - late String gitDiffResponse; - String? mergeBaseResponse; - late MockGitDir gitDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - createPackagesDirectory(fileSystem: fileSystem); - gitDirCommands = ?>[]; - gitDiffResponse = ''; - gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - gitDirCommands.add(invocation.positionalArguments[0] as List?); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (invocation.positionalArguments[0][0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } else if (invocation.positionalArguments[0][0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn(mergeBaseResponse); - } - return Future.value(mockProcessResult); - }); - processRunner = RecordingProcessRunner(); - }); - - test('No git diff should result no files changed', () async { - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, isEmpty); - }); - - test('get correct files changed based on git diff', () async { - gitDiffResponse = ''' -file1/file1.cc -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect( - changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); - }); - - test('get correct pubspec change based on git diff', () async { - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedPubSpecs(); - - expect(changedFiles, equals(['file1/pubspec.yaml'])); - }); - - test('use correct base sha if not specified', () async { - mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - - final GitVersionFinder finder = GitVersionFinder(gitDir, null); - await finder.getChangedFiles(); - verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); - }); - - test('use correct base sha if specified', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(); - verify(gitDir - .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); - }); - }); - - group('$PubVersionFinder', () { - test('Package does not exist.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 404); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse.statusCode, 404); - expect(response.httpResponse.body, ''); - }); - - test('HTTP error when getting versions from pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 400); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse.statusCode, 400); - expect(response.httpResponse.body, ''); - }); - - test('Get a correct list of versions when http response is OK.', () async { - const Map httpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '0.0.2+2', - '0.1.1', - '0.0.1+1', - '0.1.0', - '0.2.0', - '0.1.0+1', - '0.0.2+1', - '2.0.0', - '1.2.0', - '1.0.0', - ], - }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(package: 'some_package'); - - expect(response.versions, [ - Version.parse('2.0.0'), - Version.parse('1.2.0'), - Version.parse('1.0.0'), - Version.parse('0.2.0'), - Version.parse('0.1.1'), - Version.parse('0.1.0+1'), - Version.parse('0.1.0'), - Version.parse('0.0.2+2'), - Version.parse('0.0.2+1'), - Version.parse('0.0.2'), - Version.parse('0.0.1+1'), - Version.parse('0.0.1'), - ]); - expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse.statusCode, 200); - expect(response.httpResponse.body, json.encode(httpResponse)); - }); - }); - - group('pluginSupportsPlatform', () { - test('no platforms', () async { - final Directory plugin = createFakePlugin('plugin', packagesDir); - - expect(pluginSupportsPlatform('android', plugin), isFalse); - expect(pluginSupportsPlatform('ios', plugin), isFalse); - expect(pluginSupportsPlatform('linux', plugin), isFalse); - expect(pluginSupportsPlatform('macos', plugin), isFalse); - expect(pluginSupportsPlatform('web', plugin), isFalse); - expect(pluginSupportsPlatform('windows', plugin), isFalse); - }); - - test('all platforms', () async { - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - expect(pluginSupportsPlatform('android', plugin), isTrue); - expect(pluginSupportsPlatform('ios', plugin), isTrue); - expect(pluginSupportsPlatform('linux', plugin), isTrue); - expect(pluginSupportsPlatform('macos', plugin), isTrue); - expect(pluginSupportsPlatform('web', plugin), isTrue); - expect(pluginSupportsPlatform('windows', plugin), isTrue); - }); - - test('some platforms', () async { - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: false, - isLinuxPlugin: true, - isMacOsPlugin: false, - isWebPlugin: true, - isWindowsPlugin: false, - ); - - expect(pluginSupportsPlatform('android', plugin), isTrue); - expect(pluginSupportsPlatform('ios', plugin), isFalse); - expect(pluginSupportsPlatform('linux', plugin), isTrue); - expect(pluginSupportsPlatform('macos', plugin), isFalse); - expect(pluginSupportsPlatform('web', plugin), isTrue); - expect(pluginSupportsPlatform('windows', plugin), isFalse); - }); - - test('inline plugins are only detected as inline', () async { - // createFakePlugin makes non-federated pubspec entries. - final Directory plugin = createFakePlugin( - 'plugin', - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.federated), - isFalse); - }); - - test('federated plugins are only detected as federated', () async { - const String pluginName = 'plugin'; - final Directory plugin = createFakePlugin( - pluginName, - packagesDir, - isAndroidPlugin: true, - isIosPlugin: true, - isLinuxPlugin: true, - isMacOsPlugin: true, - isWebPlugin: true, - isWindowsPlugin: true, - ); - - createFakePubspec( - plugin, - name: pluginName, - androidSupport: PlatformSupport.federated, - iosSupport: PlatformSupport.federated, - linuxSupport: PlatformSupport.federated, - macosSupport: PlatformSupport.federated, - webSupport: PlatformSupport.federated, - windowsSupport: PlatformSupport.federated, - ); - - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('android', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('ios', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('linux', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('macos', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('web', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform('windows', plugin, - requiredMode: PlatformSupport.inline), - isFalse); - }); - }); } class SamplePluginCommand extends PluginCommand { diff --git a/script/tool/test/common_test.mocks.dart b/script/tool/test/common/plugin_command_test.mocks.dart similarity index 100% rename from script/tool/test/common_test.mocks.dart rename to script/tool/test/common/plugin_command_test.mocks.dart diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart new file mode 100644 index 000000000000..aaa850155da4 --- /dev/null +++ b/script/tool/test/common/plugin_utils_test.dart @@ -0,0 +1,210 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('pluginSupportsPlatform', () { + test('no platforms', () async { + final Directory plugin = createFakePlugin('plugin', packagesDir); + + expect(pluginSupportsPlatform('android', plugin), isFalse); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isFalse); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isFalse); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('all platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isTrue); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isTrue); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isTrue); + }); + + test('some platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: false, + isLinuxPlugin: true, + isMacOsPlugin: false, + isWebPlugin: true, + isWindowsPlugin: false, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('inline plugins are only detected as inline', () async { + // createFakePlugin makes non-federated pubspec entries. + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + }); + + test('federated plugins are only detected as federated', () async { + const String pluginName = 'plugin'; + final Directory plugin = createFakePlugin( + pluginName, + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + createFakePubspec( + plugin, + name: pluginName, + androidSupport: PlatformSupport.federated, + iosSupport: PlatformSupport.federated, + linuxSupport: PlatformSupport.federated, + macosSupport: PlatformSupport.federated, + webSupport: PlatformSupport.federated, + windowsSupport: PlatformSupport.federated, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + }); + }); +} diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart new file mode 100644 index 000000000000..7d8658a907ee --- /dev/null +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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:flutter_plugin_tools/src/common/pub_version_finder.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('Package does not exist.', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 404); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.noPackageFound); + expect(response.httpResponse.statusCode, 404); + expect(response.httpResponse.body, ''); + }); + + test('HTTP error when getting versions from pub', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 400); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.fail); + expect(response.httpResponse.statusCode, 400); + expect(response.httpResponse.body, ''); + }); + + test('Get a correct list of versions when http response is OK.', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '0.0.2+2', + '0.1.1', + '0.0.1+1', + '0.1.0', + '0.2.0', + '0.1.0+1', + '0.0.2+1', + '2.0.0', + '1.2.0', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, [ + Version.parse('2.0.0'), + Version.parse('1.2.0'), + Version.parse('1.0.0'), + Version.parse('0.2.0'), + Version.parse('0.1.1'), + Version.parse('0.1.0+1'), + Version.parse('0.1.0'), + Version.parse('0.0.2+2'), + Version.parse('0.0.2+1'), + Version.parse('0.0.2'), + Version.parse('0.0.1+1'), + Version.parse('0.0.1'), + ]); + expect(response.result, PubVersionFinderResult.success); + expect(response.httpResponse.statusCode, 200); + expect(response.httpResponse.body, json.encode(httpResponse)); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index c9a8b9d90a83..9c5bd18cfb11 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; import 'package:path/path.dart' as p; import 'package:platform/platform.dart'; diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart index aa8be17d6794..0bc8f1e197c6 100644 --- a/script/tool/test/firebase_test_lab_test.dart +++ b/script/tool/test/firebase_test_lab_test.dart @@ -7,7 +7,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart index a874d7db17b7..dfe8d25197ab 100644 --- a/script/tool/test/license_check_command_test.dart +++ b/script/tool/test/license_check_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/license_check_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index e5722567f20c..c0ccd2989cf9 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -9,7 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/publish_check_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart index 1cb4245fdb73..ef682bfe61f6 100644 --- a/script/tool/test/publish_plugin_command_test.dart +++ b/script/tool/test/publish_plugin_command_test.dart @@ -9,7 +9,8 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; import 'package:git/git.dart'; import 'package:mockito/mockito.dart'; diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart index 576060d23a9d..f5fe6aef849a 100644 --- a/script/tool/test/pubspec_check_command_test.dart +++ b/script/tool/test/pubspec_check_command_test.dart @@ -5,7 +5,7 @@ import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; import 'package:test/test.dart'; diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart index c590d8a4bb04..79c46fcc50e5 100644 --- a/script/tool/test/util.dart +++ b/script/tool/test/util.dart @@ -9,7 +9,8 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; import 'package:meta/meta.dart'; import 'package:quiver/collection.dart'; diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart index 1199c270642f..a8e7e20bad24 100644 --- a/script/tool/test/version_check_test.dart +++ b/script/tool/test/version_check_test.dart @@ -9,7 +9,7 @@ import 'dart:io' as io; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/version_check_command.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; @@ -17,7 +17,7 @@ import 'package:mockito/mockito.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -import 'common_test.mocks.dart'; +import 'common/plugin_command_test.mocks.dart'; import 'util.dart'; void testAllowedVersion( diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart index 8ed8144562c9..c0bd6b5dee50 100644 --- a/script/tool/test/xctest_command_test.dart +++ b/script/tool/test/xctest_command_test.dart @@ -7,7 +7,8 @@ import 'dart:convert'; import 'package:args/command_runner.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; import 'package:flutter_plugin_tools/src/xctest_command.dart'; import 'package:test/test.dart';